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

Added profile sharing and setting nicknames.
pull/65/head
sachaaaaa 7 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": { "cancel": {
"message": "Cancel" "message": "Cancel"
}, },
"clear": {
"message": "Clear"
},
"failedToSend": { "failedToSend": {
"message": "message":
"Failed to send to some recipients. Check your network connection." "Failed to send to some recipients. Check your network connection."
@ -1346,6 +1349,14 @@
"message": "Disappearing messages", "message": "Disappearing messages",
"description": "Conversation menu option to enable 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": { "timerOption_0_seconds_abbreviated": {
"message": "off", "message": "off",
"description": "description":
@ -1637,5 +1648,13 @@
"settingsUnblockHeader": { "settingsUnblockHeader": {
"message": "Blocked Users", "message": "Blocked Users",
"description": "Shown in the settings page as the heading for the blocked user settings" "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>
</div> </div>
<div class='identityKeyWrapper'> <div class='identity-key-placeholder'></div>
Your identity key: <span class='identityKey'>{{ identityKey }}</span>
</div>
<div class='underneathIdentityWrapper'> <div class='underneathIdentityWrapper'>
<div class='conversation-stack'> <div class='conversation-stack'>
<div class='conversation placeholder'> <div class='conversation placeholder'>
@ -150,6 +148,22 @@
<span class='time'>0:00</span> <span class='time'>0:00</span>
<button class='close'><span class='icon'></span></button> <button class='close'><span class='icon'></span></button>
</script> </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'> <script type='text/x-tmpl-mustache' id='confirmation-dialog'>
<div class="content"> <div class="content">
{{ #title }} {{ #title }}
@ -608,6 +622,7 @@
<script type='text/javascript' src='js/models/messages.js'></script> <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/conversations.js'></script>
<script type='text/javascript' src='js/models/blockedNumbers.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/expiring_messages.js'></script>
<script type='text/javascript' src='js/chromium.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/inbox_view.js'></script>
<script type='text/javascript' src='js/views/network_status_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/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/identicon_svg_view.js'></script>
<script type='text/javascript' src='js/views/install_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> <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 }) => { Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => {
try { try {
const conversation = ConversationController.get(pubKey); const conversation = ConversationController.get(pubKey);

@ -226,6 +226,9 @@
await Promise.all( await Promise.all(
conversations.map(conversation => conversation.updateLastMessage()) conversations.map(conversation => conversation.updateLastMessage())
); );
// Update profiles
conversations.map(conversation => conversation.updateProfile());
window.log.info('ConversationController: done with initial fetch'); window.log.info('ConversationController: done with initial fetch');
} catch (error) { } catch (error) {
window.log.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() { onChangeProfileKey() {
if (this.isPrivate()) { if (this.isPrivate()) {
this.getProfiles(); this.getProfiles();
@ -1671,148 +1712,22 @@
return Promise.all(_.map(ids, this.getProfile)); 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) { async getProfile(id) {
if (!textsecure.messaging) {
throw new Error(
'Conversation.getProfile: textsecure.messaging not available'
);
}
const c = await ConversationController.getOrCreateAndWait(id, 'private'); const c = await ConversationController.getOrCreateAndWait(id, 'private');
// Because we're no longer using Backbone-integrated saves, we need to manually // We only need to update the profile as they are all stored inside the conversation
// clear the changed fields here so our hasChanged() check is useful. await c.updateProfile();
c.changed = {}; },
async setProfileName(name) {
try { const profileName = this.get('profileName');
await c.deriveAccessKeyIfNeeded(); if (profileName !== name) {
const numberInfo = c.getNumberInfo({ disableMeCheck: true }) || {}; this.set({ profileName: name });
const getInfo = numberInfo[c.id] || {}; await window.Signal.Data.updateConversation(this.id, this.attributes, {
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, Conversation: Whisper.Conversation,
}); });
} }
}
},
async setProfileName(encryptedName) {
if (!encryptedName) {
return;
}
const key = this.get('profileKey');
if (!key) {
return;
}
// 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) { async setProfileAvatar(avatarPath) {
if (!avatarPath) { if (!avatarPath) {
@ -1989,7 +1904,10 @@
getTitle() { getTitle() {
if (this.isPrivate()) { 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'; 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, MediaGallery,
} = require('../../ts/components/conversation/media-gallery/MediaGallery'); } = require('../../ts/components/conversation/media-gallery/MediaGallery');
const { MainHeader } = require('../../ts/components/MainHeader'); const { MainHeader } = require('../../ts/components/MainHeader');
const { IdentityKeyHeader } = require('../../ts/components/IdentityKeyHeader');
const { Message } = require('../../ts/components/conversation/Message'); const { Message } = require('../../ts/components/conversation/Message');
const { MessageBody } = require('../../ts/components/conversation/MessageBody'); const { MessageBody } = require('../../ts/components/conversation/MessageBody');
const { const {
@ -184,6 +185,7 @@ exports.setup = (options = {}) => {
Lightbox, Lightbox,
LightboxGallery, LightboxGallery,
MainHeader, MainHeader,
IdentityKeyHeader,
MediaGallery, MediaGallery,
Message, Message,
MessageBody, 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(), name: item.getName(),
value: item.get('seconds'), value: item.get('seconds'),
})), })),
hasNickname: !!this.model.getNickname(),
onSetDisappearingMessages: seconds => onSetDisappearingMessages: seconds =>
this.setDisappearingMessages(seconds), this.setDisappearingMessages(seconds),
@ -204,6 +205,16 @@
onUnblockUser: () => { onUnblockUser: () => {
this.model.unblock(); 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({ this.titleView = new Whisper.ReactWrapperView({

@ -6,6 +6,7 @@
/* global textsecure: false */ /* global textsecure: false */
/* global Signal: false */ /* global Signal: false */
/* global StringView: false */ /* global StringView: false */
/* global storage: false */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function() {
@ -42,7 +43,7 @@
if ($el && $el.length > 0) { if ($el && $el.length > 0) {
$el.remove(); $el.remove();
} }
} },
}); });
Whisper.FontSizeView = Whisper.View.extend({ Whisper.FontSizeView = Whisper.View.extend({
@ -113,6 +114,16 @@
this.listenTo(me, 'change', update); this.listenTo(me, 'change', update);
this.$('.main-header-placeholder').append(this.mainHeaderView.el); 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({ this.conversation_stack = new Whisper.ConversationStack({
el: this.$('.conversation-stack'), el: this.$('.conversation-stack'),
model: { window: options.window }, model: { window: options.window },
@ -184,14 +195,26 @@
this.$el.addClass('expired'); this.$el.addClass('expired');
} }
}, },
render_attributes() { _getIdentityKeyViewProps() {
const identityKey = textsecure.storage.get('identityKey').pubKey; 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 { return {
welcomeToSignal: i18n('welcomeToSignal'), welcomeToSignal: i18n('welcomeToSignal'),
selectAContact: i18n('selectAContact'), selectAContact: i18n('selectAContact'),
searchForPeopleOrGroups: i18n('searchForPeopleOrGroups'), searchForPeopleOrGroups: i18n('searchForPeopleOrGroups'),
settings: i18n('settings'), settings: i18n('settings'),
identityKey: StringView.arrayBufferToHex(identityKey),
}; };
}, },
events: { 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 window: false */
/* global textsecure: false */ /* global textsecure: false */
/* global storage: false */
/* global StringView: false */ /* global StringView: false */
/* global libloki: false */ /* global libloki: false */
/* global libsignal: false */ /* global libsignal: false */
@ -918,11 +919,23 @@ MessageReceiver.prototype.extend({
const groupId = message.group && message.group.id; const groupId = message.group && message.group.id;
const isBlocked = this.isGroupBlocked(groupId); const isBlocked = this.isGroupBlocked(groupId);
const isMe = envelope.source === textsecure.storage.user.getNumber(); const isMe = envelope.source === textsecure.storage.user.getNumber();
const conversation = window.ConversationController.get(envelope.source);
const isLeavingGroup = Boolean( const isLeavingGroup = Boolean(
message.group && message.group &&
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT 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) { if (type === 'friend-request' && isMe) {
window.log.info( window.log.info(
'refusing to add a friend request to ourselves' 'refusing to add a friend request to ourselves'

@ -25,6 +25,7 @@ function Message(options) {
this.needsSync = options.needsSync; this.needsSync = options.needsSync;
this.expireTimer = options.expireTimer; this.expireTimer = options.expireTimer;
this.profileKey = options.profileKey; this.profileKey = options.profileKey;
this.profile = options.profile;
if (!(this.recipients instanceof Array) || this.recipients.length < 1) { if (!(this.recipients instanceof Array) || this.recipients.length < 1) {
throw new Error('Invalid recipient list'); throw new Error('Invalid recipient list');
@ -132,6 +133,12 @@ Message.prototype = {
proto.profileKey = this.profileKey; 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; this.dataMessage = proto;
return proto; return proto;
}, },
@ -656,6 +663,7 @@ MessageSender.prototype = {
profileKey, profileKey,
options options
) { ) {
const profile = textsecure.storage.impl.getLocalProfile();
return this.sendMessage( return this.sendMessage(
{ {
recipients: [number], recipients: [number],
@ -666,6 +674,7 @@ MessageSender.prototype = {
needsSync: true, needsSync: true,
expireTimer, expireTimer,
profileKey, profileKey,
profile,
}, },
options options
); );

@ -176,6 +176,7 @@ message DataMessage {
optional uint64 timestamp = 7; optional uint64 timestamp = 7;
optional Quote quote = 8; optional Quote quote = 8;
repeated Contact contact = 9; repeated Contact contact = 9;
optional Contact profile = 101; // Loki: The profile of the current user
} }
message NullMessage { 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, .permissions-popup,
.debug-log-window { .debug-log-window {
.modal { .modal {

@ -71,21 +71,51 @@
} }
} }
.identityKeyWrapper { .identity-key-wrapper {
background-color: $color-black-008-no-tranparency; background-color: $color-black-008-no-tranparency;
text-align: center; display: flex;
height: 50px; flex: 1;
line-height: 50px; 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; white-space: nowrap;
overflow-x: hidden;
} }
.identityKey { .identity-key-text-container {
flex: 1;
text-align: center;
flex-direction: column;
}
.identity-key-container div {
overflow-x: hidden;
text-overflow: ellipsis;
}
.identity-key_bold {
font-weight: 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 { .underneathIdentityWrapper {
position: absolute; position: absolute;
top: 50px; top: 60px;
bottom: 0; bottom: 0;
left: 300px; left: 300px;
right: 0; right: 0;

@ -1,8 +1,20 @@
// Using BEM syntax explained here: https://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/ // Using BEM syntax explained here: https://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/
// Module: Contact Name // 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; font-style: italic;
} }

@ -6,6 +6,16 @@ body.dark-theme {
} }
.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
.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 { .conversation-loading-screen {
background-color: $color-gray-95; background-color: $color-gray-95;
} }

@ -127,6 +127,17 @@
<span class='time'>0:00</span> <span class='time'>0:00</span>
<button class='close'><span class='icon'></span></button> <button class='close'><span class='icon'></span></button>
</script> </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'> <script type='text/x-tmpl-mustache' id='confirmation-dialog'>
<div class="content"> <div class="content">
<div class='message'>{{ message }}</div> <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/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/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/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/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/last_seen_indicator_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/scroll_down_button_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 shouldShowProfile = Boolean(profileName && !name);
const profileElement = shouldShowProfile ? ( const profileElement = shouldShowProfile ? (
<span className={`${prefix}__profile-name`}> <span className={`${prefix}__profile-name`}>
~<Emojify text={profileName || ''} i18n={i18n} /> <Emojify text={profileName || ''} i18n={i18n} />
</span> </span>
) : null; ) : null;
return ( return (
<span className={prefix}> <span className={prefix}>
<Emojify text={title} i18n={i18n} />
{shouldShowProfile ? ' ' : null}
{profileElement} {profileElement}
<span className={shouldShowProfile ? `${prefix}__profile-number` : ''}>
<Emojify text={title} i18n={i18n} />
</span>
</span> </span>
); );
} }

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Emojify } from './Emojify'; import { ContactName } from './ContactName';
import { Avatar } from '../Avatar'; import { Avatar } from '../Avatar';
import { Localizer } from '../../types/Util'; import { Localizer } from '../../types/Util';
import { import {
@ -36,6 +36,7 @@ interface Props {
expirationSettingName?: string; expirationSettingName?: string;
showBackButton: boolean; showBackButton: boolean;
timerOptions: Array<TimerOption>; timerOptions: Array<TimerOption>;
hasNickname?: boolean;
onSetDisappearingMessages: (seconds: number) => void; onSetDisappearingMessages: (seconds: number) => void;
onDeleteMessages: () => void; onDeleteMessages: () => void;
@ -48,6 +49,9 @@ interface Props {
onBlockUser: () => void; onBlockUser: () => void;
onUnblockUser: () => void; onUnblockUser: () => void;
onClearNickname: () => void;
onChangeNickname: () => void;
} }
export class ConversationHeader extends React.Component<Props> { export class ConversationHeader extends React.Component<Props> {
@ -90,31 +94,19 @@ export class ConversationHeader extends React.Component<Props> {
public renderTitle() { public renderTitle() {
const { const {
name,
phoneNumber, phoneNumber,
i18n, i18n,
profileName, profileName,
isVerified,
isKeysPending, isKeysPending,
} = this.props; } = this.props;
return ( return (
<div className="module-conversation-header__title"> <div className="module-conversation-header__title">
{name ? <Emojify text={name} i18n={i18n} /> : null} <ContactName
{name && phoneNumber ? ' · ' : null} phoneNumber={phoneNumber}
{phoneNumber ? phoneNumber : null}{' '} profileName={profileName}
{profileName && !name ? ( i18n={i18n}
<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} {isKeysPending ? ' (pending)' : null}
</div> </div>
); );
@ -198,6 +190,9 @@ export class ConversationHeader extends React.Component<Props> {
timerOptions, timerOptions,
onBlockUser, onBlockUser,
onUnblockUser, onUnblockUser,
hasNickname,
onClearNickname,
onChangeNickname,
} = this.props; } = this.props;
const disappearingTitle = i18n('disappearingMessages') as any; const disappearingTitle = i18n('disappearingMessages') as any;
@ -237,6 +232,12 @@ export class ConversationHeader extends React.Component<Props> {
{!isMe ? ( {!isMe ? (
<MenuItem onClick={blockHandler}>{blockTitle}</MenuItem> <MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>
) : null} ) : null}
{!isMe ? (
<MenuItem onClick={onChangeNickname}>{i18n('changeNickname')}</MenuItem>
) : null}
{!isMe && hasNickname ? (
<MenuItem onClick={onClearNickname}>{i18n('clearNickname')}</MenuItem>
) : null}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem> <MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
</ContextMenu> </ContextMenu>
); );

Loading…
Cancel
Save