Merge pull request #610 from msgmaxim/profile-pictures

Enable custom profile pictures
pull/651/head
Maxim Shishmarev 6 years ago committed by GitHub
commit 8cd8c8d4f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2038,10 +2038,9 @@
"description": "description":
"A toast message telling the user that the message text was copied" "A toast message telling the user that the message text was copied"
}, },
"editDisplayName": { "editProfile": {
"message": "Edit display name", "message": "Edit profile",
"description": "description": "Button action that the user can click to edit their profile"
"Button action that the user can click to edit their display name"
}, },
"createGroupDialogTitle": { "createGroupDialogTitle": {
@ -2212,12 +2211,22 @@
"message": "Group Name cannot be empty", "message": "Group Name cannot be empty",
"description": "Error message displayed on empty group name" "description": "Error message displayed on empty group name"
}, },
"emptyProfileNameError": {
"message": "Profile name cannot be empty",
"description": "Error message displayed on empty profile name"
},
"maxGroupMembersError": { "maxGroupMembersError": {
"message": "Max number of members for small group chats is: " "message": "Max number of members for small group chats is: "
}, },
"nonAdminDeleteMember": { "nonAdminDeleteMember": {
"message": "Only group admin can remove members!" "message": "Only group admin can remove members!"
}, },
"editProfileDialogTitle": {
"message": "Editing Profile"
},
"profileName": {
"message": "Profile Name"
},
"groupNamePlaceholder": { "groupNamePlaceholder": {
"message": "Group Name" "message": "Group Name"
} }

@ -816,6 +816,7 @@
<script type='text/javascript' src='js/views/device_pairing_dialog_view.js'></script> <script type='text/javascript' src='js/views/device_pairing_dialog_view.js'></script>
<script type='text/javascript' src='js/views/device_pairing_words_dialog_view.js'></script> <script type='text/javascript' src='js/views/device_pairing_words_dialog_view.js'></script>
<script type='text/javascript' src='js/views/create_group_dialog_view.js'></script> <script type='text/javascript' src='js/views/create_group_dialog_view.js'></script>
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
<script type='text/javascript' src='js/wall_clock_listener.js'></script> <script type='text/javascript' src='js/wall_clock_listener.js'></script>
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script> <script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>

@ -841,15 +841,70 @@
ourNumber, ourNumber,
'private' 'private'
); );
const readFile = attachment =>
new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = e => {
const data = e.target.result;
resolve({
...attachment,
data,
size: data.byteLength,
});
};
fileReader.onerror = reject;
fileReader.onabort = reject;
fileReader.readAsArrayBuffer(attachment.file);
});
const avatarPath = conversation.getAvatarPath();
const profile = conversation.getLokiProfile(); const profile = conversation.getLokiProfile();
const displayName = profile && profile.displayName; const displayName = profile && profile.displayName;
if (appView) { if (appView) {
appView.showNicknameDialog({ appView.showEditProfileDialog({
title: window.i18n('editProfileTitle'), profileName: displayName,
message: window.i18n('editProfileDisplayNameWarning'), pubkey: ourNumber,
nickname: displayName, avatarPath,
onOk: newName => avatarColor: conversation.getColor(),
conversation.setLokiProfile({ displayName: newName }), onOk: async (newName, avatar) => {
let newAvatarPath = '';
if (avatar) {
const data = await readFile({ file: avatar });
// For simplicity we use the same attachment pointer that would send to
// others, which means we need to wait for the database response.
// To avoid the wait, we create a temporary url for the local image
// and use it until we the the response from the server
const tempUrl = window.URL.createObjectURL(avatar);
conversation.setLokiProfile({ displayName: newName });
conversation.set('avatar', tempUrl);
const avatarPointer = await textsecure.messaging.uploadAvatar(
data
);
conversation.set('avatarPointer', avatarPointer.url);
const downloaded = await messageReceiver.downloadAttachment({
url: avatarPointer.url,
isRaw: true,
});
const upgraded = await Signal.Migrations.processNewAttachment(
downloaded
);
newAvatarPath = upgraded.path;
}
// Replace our temporary image with the attachment pointer from the server:
conversation.set('avatar', null);
conversation.setLokiProfile({
displayName: newName,
avatar: newAvatarPath,
});
},
}); });
} }
}); });

@ -2255,14 +2255,18 @@
await this.updateProfileName(); await this.updateProfileName();
}, },
async setLokiProfile(profile) { async setLokiProfile(newProfile) {
if (!_.isEqual(this.get('profile'), profile)) { if (!_.isEqual(this.get('profile'), newProfile)) {
this.set({ profile }); this.set({ profile: newProfile });
await window.Signal.Data.updateConversation(this.id, this.attributes, { await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation, Conversation: Whisper.Conversation,
}); });
} }
if (newProfile.avatar) {
await this.setProfileAvatar({ path: newProfile.avatar });
}
await this.updateProfileName(); await this.updateProfileName();
}, },
async updateProfileName() { async updateProfileName() {
@ -2435,10 +2439,10 @@
}); });
} }
}, },
async setProfileAvatar(avatarPath) { async setProfileAvatar(avatar) {
const profileAvatar = this.get('profileAvatar'); const profileAvatar = this.get('profileAvatar');
if (profileAvatar !== avatarPath) { if (profileAvatar !== avatar) {
this.set({ profileAvatar: avatarPath }); this.set({ profileAvatar: avatar });
await window.Signal.Data.updateConversation(this.id, this.attributes, { await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation, Conversation: Whisper.Conversation,
}); });
@ -2749,22 +2753,20 @@
getAvatarPath() { getAvatarPath() {
const avatar = this.get('avatar') || this.get('profileAvatar'); const avatar = this.get('avatar') || this.get('profileAvatar');
if (avatar) { if (typeof avatar === 'string') {
if (avatar.path) {
return getAbsoluteAttachmentPath(avatar.path);
}
return avatar; return avatar;
} }
if (avatar && avatar.path && typeof avatar.path === 'string') {
return getAbsoluteAttachmentPath(avatar.path);
}
return null; return null;
}, },
getAvatar() { getAvatar() {
const title = this.get('name'); const title = this.get('name');
const color = this.getColor(); const color = this.getColor();
const avatar = this.get('avatar') || this.get('profileAvatar'); const url = this.getAvatarPath();
const url =
avatar && avatar.path ? getAbsoluteAttachmentPath(avatar.path) : avatar;
if (url) { if (url) {
return { url, color }; return { url, color };

@ -2180,8 +2180,6 @@
} else { } else {
sendingDeviceConversation.setProfileKey(profileKey); sendingDeviceConversation.setProfileKey(profileKey);
} }
} else if (dataMessage.profile) {
sendingDeviceConversation.setLokiProfile(dataMessage.profile);
} }
let autoAccept = false; let autoAccept = false;

@ -446,6 +446,36 @@ class LokiAppDotNetServerAPI {
return false; return false;
} }
async uploadAvatar(data) {
const endpoint = 'users/me/avatar';
const options = {
method: 'POST',
rawBody: data,
};
const { statusCode, response } = await this.serverRequest(
endpoint,
options
);
if (statusCode !== 200) {
log.warn('Failed to upload avatar to fileserver');
return null;
}
const url =
response.data &&
response.data.avatar_image &&
response.data.avatar_image.url;
// We don't use the server id for avatars
return {
url,
id: null,
};
}
async uploadData(data) { async uploadData(data) {
const endpoint = 'files'; const endpoint = 'files';
const options = { const options = {
@ -760,7 +790,7 @@ class LokiPublicChannelAPI {
} }
// timestamp is the only required field we've had since the first deployed version // timestamp is the only required field we've had since the first deployed version
const { timestamp, quote } = noteValue; const { timestamp, quote, avatar } = noteValue;
if (quote) { if (quote) {
// TODO: Enable quote attachments again using proper ADN style // TODO: Enable quote attachments again using proper ADN style
@ -823,6 +853,7 @@ class LokiPublicChannelAPI {
attachments, attachments,
preview, preview,
quote, quote,
avatar,
}; };
} }
@ -889,7 +920,13 @@ class LokiPublicChannelAPI {
return false; return false;
} }
const { timestamp, quote, attachments, preview } = messengerData; const {
timestamp,
quote,
attachments,
preview,
avatar,
} = messengerData;
if (!timestamp) { if (!timestamp) {
return false; // Invalid message return false; // Invalid message
} }
@ -924,6 +961,7 @@ class LokiPublicChannelAPI {
].splice(-5); ].splice(-5);
const from = adnMessage.user.name || 'Anonymous'; // profileName const from = adnMessage.user.name || 'Anonymous'; // profileName
const avatarObj = avatar || null;
// track sources for multidevice support // track sources for multidevice support
if (pubKeys.indexOf(`@${adnMessage.user.username}`) === -1) { if (pubKeys.indexOf(`@${adnMessage.user.username}`) === -1) {
@ -961,6 +999,7 @@ class LokiPublicChannelAPI {
preview, preview,
profile: { profile: {
displayName: from, displayName: from,
avatar: avatarObj,
}, },
}, },
}; };
@ -1143,6 +1182,8 @@ class LokiPublicChannelAPI {
LokiPublicChannelAPI.getAnnotationFromPreview LokiPublicChannelAPI.getAnnotationFromPreview
); );
const avatarAnnotation = data.profile.avatar || null;
const payload = { const payload = {
text, text,
annotations: [ annotations: [
@ -1150,12 +1191,14 @@ class LokiPublicChannelAPI {
type: 'network.loki.messenger.publicChat', type: 'network.loki.messenger.publicChat',
value: { value: {
timestamp: messageTimeStamp, timestamp: messageTimeStamp,
avatar: avatarAnnotation,
}, },
}, },
...attachmentAnnotations, ...attachmentAnnotations,
...previewAnnotations, ...previewAnnotations,
], ],
}; };
if (quote && quote.id) { if (quote && quote.id) {
payload.annotations[0].value.quote = quote; payload.annotations[0].value.quote = quote;

@ -217,6 +217,10 @@ class LokiFileServerAPI {
); );
} }
uploadAvatar(data) {
return this._server.uploadAvatar(data);
}
uploadPrivateAttachment(data) { uploadPrivateAttachment(data) {
return this._server.uploadData(data); return this._server.uploadData(data);
} }

@ -48,6 +48,7 @@ const { BulkEdit } = require('../../ts/components/conversation/BulkEdit');
const { const {
CreateGroupDialog, CreateGroupDialog,
} = require('../../ts/components/conversation/CreateGroupDialog'); } = require('../../ts/components/conversation/CreateGroupDialog');
const { EditProfileDialog } = require('../../ts/components/EditProfileDialog');
const { const {
UpdateGroupDialog, UpdateGroupDialog,
} = require('../../ts/components/conversation/UpdateGroupDialog'); } = require('../../ts/components/conversation/UpdateGroupDialog');
@ -228,6 +229,7 @@ exports.setup = (options = {}) => {
MainHeader, MainHeader,
MemberList, MemberList,
CreateGroupDialog, CreateGroupDialog,
EditProfileDialog,
ConfirmDialog, ConfirmDialog,
UpdateGroupDialog, UpdateGroupDialog,
BulkEdit, BulkEdit,

@ -463,6 +463,7 @@ function initialize({
getSenderCertificate, getSenderCertificate,
makeProxiedRequest, makeProxiedRequest,
putAttachment, putAttachment,
putAvatar,
registerKeys, registerKeys,
registerSupportForUnauthenticatedDelivery, registerSupportForUnauthenticatedDelivery,
removeSignalingKey, removeSignalingKey,
@ -854,9 +855,9 @@ function initialize({
}); });
} }
function putAttachment(encryptedBin) { function putAttachment(maybeEncryptedBin) {
const formData = new FormData(); const formData = new FormData();
const buffer = Buffer.from(encryptedBin); const buffer = Buffer.from(maybeEncryptedBin);
formData.append('type', 'network.loki'); formData.append('type', 'network.loki');
formData.append('content', buffer, { formData.append('content', buffer, {
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
@ -867,6 +868,17 @@ function initialize({
return lokiFileServerAPI.uploadPrivateAttachment(formData); return lokiFileServerAPI.uploadPrivateAttachment(formData);
} }
function putAvatar(bin) {
const formData = new FormData();
const buffer = Buffer.from(bin);
formData.append('avatar', buffer, {
contentType: 'application/octet-stream',
name: 'avatar',
filename: 'attachment',
});
return lokiFileServerAPI.uploadAvatar(formData);
}
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
async function getProxiedSize(url) { async function getProxiedSize(url) {
const result = await _outerAjax(url, { const result = await _outerAjax(url, {

@ -176,6 +176,10 @@
}); });
} }
}, },
showEditProfileDialog(options) {
const dialog = new Whisper.EditProfileDialogView(options);
this.el.append(dialog.el);
},
showNicknameDialog({ pubKey, title, message, nickname, onOk, onCancel }) { showNicknameDialog({ pubKey, title, message, nickname, onOk, onCancel }) {
const _title = title || `Change nickname for ${pubKey}`; const _title = title || `Change nickname for ${pubKey}`;
const dialog = new Whisper.NicknameDialogView({ const dialog = new Whisper.NicknameDialogView({

@ -0,0 +1,44 @@
/* global i18n, Whisper */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.EditProfileDialogView = Whisper.View.extend({
className: 'loki-dialog modal',
initialize({ profileName, avatarPath, avatarColor, pubkey, onOk }) {
this.close = this.close.bind(this);
this.profileName = profileName;
this.pubkey = pubkey;
this.avatarPath = avatarPath;
this.avatarColor = avatarColor;
this.onOk = onOk;
this.$el.focus();
this.render();
},
render() {
this.dialogView = new Whisper.ReactWrapperView({
className: 'edit-profile-dialog',
Component: window.Signal.Components.EditProfileDialog,
props: {
onOk: this.onOk,
onClose: this.close,
profileName: this.profileName,
pubkey: this.pubkey,
avatarPath: this.avatarPath,
i18n,
},
});
this.$el.append(this.dialogView.el);
return this;
},
close() {
this.remove();
},
});
})();

@ -22,6 +22,7 @@
/* global lokiFileServerAPI: false */ /* global lokiFileServerAPI: false */
/* global WebAPI: false */ /* global WebAPI: false */
/* global ConversationController: false */ /* global ConversationController: false */
/* global Signal: false */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
/* eslint-disable no-unreachable */ /* eslint-disable no-unreachable */
@ -164,7 +165,16 @@ MessageReceiver.prototype.extend({
}; };
this.httpPollingResource.handleMessage(message, options); this.httpPollingResource.handleMessage(message, options);
}, },
handleUnencryptedMessage({ message }) { async handleUnencryptedMessage({ message }) {
const isMe = message.source === textsecure.storage.user.getNumber();
if (!isMe && message.message.profile) {
const conversation = await window.ConversationController.getOrCreateAndWait(
message.source,
'private'
);
await this.updateProfile(conversation, message.message.profile);
}
const ev = new Event('message'); const ev = new Event('message');
ev.confirm = function confirmTerm() {}; ev.confirm = function confirmTerm() {};
ev.data = message; ev.data = message;
@ -1228,6 +1238,35 @@ MessageReceiver.prototype.extend({
return true; return true;
}, },
async updateProfile(conversation, profile) {
// Retain old values unless changed:
const newProfile = conversation.get('profile') || {};
newProfile.displayName = profile.displayName;
// TODO: may need to allow users to reset their avatars to null
if (profile.avatar) {
const prevPointer = conversation.get('avatarPointer');
const needsUpdate =
!prevPointer || !_.isEqual(prevPointer, profile.avatar);
if (needsUpdate) {
conversation.set('avatarPointer', profile.avatar);
const downloaded = await this.downloadAttachment({
url: profile.avatar,
isRaw: true,
});
const upgraded = await Signal.Migrations.processNewAttachment(
downloaded
);
newProfile.avatar = upgraded.path;
}
}
await conversation.setLokiProfile(newProfile);
},
handleDataMessage(envelope, msg) { handleDataMessage(envelope, msg) {
if (!envelope.isP2p) { if (!envelope.isP2p) {
const timestamp = envelope.timestamp.toNumber(); const timestamp = envelope.timestamp.toNumber();
@ -1258,11 +1297,8 @@ MessageReceiver.prototype.extend({
// Check if we need to update any profile names // Check if we need to update any profile names
if (!isMe && conversation) { if (!isMe && conversation) {
let profile = null;
if (message.profile) { if (message.profile) {
profile = JSON.parse(message.profile.encodeJSON()); await this.updateProfile(conversation, message.profile);
// Update the conversation
await conversation.setLokiProfile(profile);
} }
} }

@ -148,6 +148,15 @@ Message.prototype = {
if (this.profile && this.profile.displayName) { if (this.profile && this.profile.displayName) {
const profile = new textsecure.protobuf.DataMessage.LokiProfile(); const profile = new textsecure.protobuf.DataMessage.LokiProfile();
profile.displayName = this.profile.displayName; profile.displayName = this.profile.displayName;
const conversation = window.ConversationController.get(
textsecure.storage.user.getNumber()
);
const avatarPointer = conversation.get('avatarPointer');
if (avatarPointer) {
profile.avatar = avatarPointer;
}
proto.profile = profile; proto.profile = profile;
} }
@ -168,7 +177,12 @@ MessageSender.prototype = {
constructor: MessageSender, constructor: MessageSender,
// makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto // makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto
async makeAttachmentPointer(attachment, publicServer = null) { async makeAttachmentPointer(
attachment,
publicServer = null,
isRaw = false,
isAvatar = false
) {
if (typeof attachment !== 'object' || attachment == null) { if (typeof attachment !== 'object' || attachment == null) {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
@ -187,9 +201,15 @@ MessageSender.prototype = {
const proto = new textsecure.protobuf.AttachmentPointer(); const proto = new textsecure.protobuf.AttachmentPointer();
let attachmentData; let attachmentData;
let server; let server;
if (publicServer) { if (publicServer) {
attachmentData = attachment.data;
server = publicServer; server = publicServer;
} else {
({ server } = this);
}
if (publicServer || isRaw) {
attachmentData = attachment.data;
} else { } else {
proto.key = libsignal.crypto.getRandomBytes(64); proto.key = libsignal.crypto.getRandomBytes(64);
const iv = libsignal.crypto.getRandomBytes(16); const iv = libsignal.crypto.getRandomBytes(16);
@ -200,10 +220,12 @@ MessageSender.prototype = {
); );
proto.digest = result.digest; proto.digest = result.digest;
attachmentData = result.ciphertext; attachmentData = result.ciphertext;
({ server } = this);
} }
const result = await server.putAttachment(attachmentData); const result = isAvatar
? await server.putAvatar(attachmentData)
: await server.putAttachment(attachmentData);
if (!result) { if (!result) {
return Promise.reject( return Promise.reject(
new Error('Failed to upload data to attachment fileserver') new Error('Failed to upload data to attachment fileserver')
@ -538,6 +560,10 @@ MessageSender.prototype = {
return this.server.getAvatar(path); return this.server.getAvatar(path);
}, },
uploadAvatar(attachment) {
return this.makeAttachmentPointer(attachment, null, true, true);
},
sendRequestConfigurationSyncMessage(options) { sendRequestConfigurationSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId(); const myDevice = textsecure.storage.user.getDeviceId();
@ -1220,6 +1246,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
this.sendSyncMessage = sender.sendSyncMessage.bind(sender); this.sendSyncMessage = sender.sendSyncMessage.bind(sender);
this.getProfile = sender.getProfile.bind(sender); this.getProfile = sender.getProfile.bind(sender);
this.getAvatar = sender.getAvatar.bind(sender); this.getAvatar = sender.getAvatar.bind(sender);
this.uploadAvatar = sender.uploadAvatar.bind(sender);
this.syncReadMessages = sender.syncReadMessages.bind(sender); this.syncReadMessages = sender.syncReadMessages.bind(sender);
this.syncVerification = sender.syncVerification.bind(sender); this.syncVerification = sender.syncVerification.bind(sender);
this.sendDeliveryReceipt = sender.sendDeliveryReceipt.bind(sender); this.sendDeliveryReceipt = sender.sendDeliveryReceipt.bind(sender);

@ -197,6 +197,7 @@ message DataMessage {
// Loki: A custom message for our profile // Loki: A custom message for our profile
message LokiProfile { message LokiProfile {
optional string displayName = 1; optional string displayName = 1;
optional string avatar = 2;
} }
optional string body = 1; optional string body = 1;

@ -10,6 +10,60 @@
overflow: hidden; overflow: hidden;
} }
.edit-profile-dialog {
.content {
max-width: 100% !important;
}
.buttons {
margin: 8px;
}
.profile-name {
font-size: larger;
text-align: center;
}
.title-text {
font-size: large;
text-align: center;
}
.message {
font-style: italic;
color: $grey;
font-size: 12px;
margin-bottom: 16px;
}
.module-avatar {
display: block;
margin-bottom: 1em;
}
.avatar-upload {
display: flex;
justify-content: center;
}
.avatar-upload-inner {
display: flex;
}
.upload-btn-background {
background-color: #ffffff70;
align-self: center;
margin-left: -24px;
margin-top: 40px;
z-index: 1;
border-radius: 8px;
}
.input-file {
display: none;
}
}
.expired { .expired {
.conversation-stack, .conversation-stack,
.gutter { .gutter {

@ -54,15 +54,20 @@
.hidden { .hidden {
display: none; display: none;
} }
}
.create-group-dialog,
.edit-profile-dialog {
.error-message { .error-message {
text-align: center; text-align: center;
color: red; color: red;
margin-bottom: 0.5em; display: block;
user-select: none;
} }
.error-faded { .error-faded {
opacity: 0; opacity: 0;
margin-top: -20px;
transition: all 100ms linear; transition: all 100ms linear;
} }

@ -98,6 +98,18 @@
right: 100%; right: 100%;
} }
.module-message__buttons__upload {
height: 24px;
width: 24px;
transform: rotate(180deg);
display: inline-block;
cursor: pointer;
@include color-svg('../images/download.svg', $color-light-45);
&:hover {
@include color-svg('../images/download.svg', $color-gray-90);
}
}
.module-message__buttons__download { .module-message__buttons__download {
min-height: 24px; min-height: 24px;
min-width: 24px; min-width: 24px;

@ -576,6 +576,7 @@
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script> <script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/clear_data_view.js'></script> <script type='text/javascript' src='../js/views/clear_data_view.js'></script>
<script type='text/javascript' src='../js/views/create_group_dialog_view.js'></script> <script type='text/javascript' src='../js/views/create_group_dialog_view.js'></script>
<script type='text/javascript' src='../js/views/edit_profile_dialog_view.js'></script>
<script type='text/javascript' src='../js/views/beta_release_disclaimer_view.js'></script> <script type='text/javascript' src='../js/views/beta_release_disclaimer_view.js'></script>

@ -0,0 +1,212 @@
import React from 'react';
import classNames from 'classnames';
import { Avatar } from './Avatar';
declare global {
interface Window {
displayNameRegex: any;
}
}
interface Props {
i18n: any;
profileName: string;
avatarPath: string;
avatarColor: string;
pubkey: string;
onClose: any;
onOk: any;
}
interface State {
profileName: string;
errorDisplayed: boolean;
errorMessage: string;
avatar: string;
}
export class EditProfileDialog extends React.Component<Props, State> {
private readonly inputEl: any;
constructor(props: any) {
super(props);
this.onNameEdited = this.onNameEdited.bind(this);
this.closeDialog = this.closeDialog.bind(this);
this.onClickOK = this.onClickOK.bind(this);
this.showError = this.showError.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.onFileSelected = this.onFileSelected.bind(this);
this.state = {
profileName: this.props.profileName,
errorDisplayed: false,
errorMessage: 'placeholder',
avatar: this.props.avatarPath,
};
this.inputEl = React.createRef();
window.addEventListener('keyup', this.onKeyUp);
}
public render() {
const i18n = this.props.i18n;
const cancelText = i18n('cancel');
const okText = i18n('ok');
const placeholderText = i18n('profileName');
const errorMessageClasses = classNames(
'error-message',
this.state.errorDisplayed ? 'error-shown' : 'error-faded'
);
return (
<div className="content">
<div className="avatar-upload">
<div className="avatar-upload-inner">
{this.renderAvatar()}
<div className="upload-btn-background">
<input
type="file"
ref={this.inputEl}
className="input-file"
placeholder="input file"
name="name"
onChange={this.onFileSelected}
/>
<div
role="button"
className={'module-message__buttons__upload'}
onClick={() => {
const el = this.inputEl.current;
if (el) {
el.click();
}
}}
/>
</div>
</div>
</div>
<input
type="text"
className="profile-name"
value={this.state.profileName}
placeholder={placeholderText}
onChange={this.onNameEdited}
tabIndex={0}
required={true}
aria-required={true}
/>
<div className="message">{i18n('editProfileDisplayNameWarning')}</div>
<span className={errorMessageClasses}>{this.state.errorMessage}</span>
<div className="buttons">
<button className="cancel" tabIndex={0} onClick={this.closeDialog}>
{cancelText}
</button>
<button className="ok" tabIndex={0} onClick={this.onClickOK}>
{okText}
</button>
</div>
</div>
);
}
private onFileSelected() {
const file = this.inputEl.current.files[0];
const url = window.URL.createObjectURL(file);
this.setState({
avatar: url,
});
}
private renderAvatar() {
const avatarPath = this.state.avatar;
const color = this.props.avatarColor;
return (
<Avatar
avatarPath={avatarPath}
color={color}
conversationType="direct"
i18n={this.props.i18n}
name={this.state.profileName}
phoneNumber={this.props.pubkey}
profileName={this.state.profileName}
size={80}
/>
);
}
private onNameEdited(e: any) {
e.persist();
const newName = e.target.value.replace(window.displayNameRegex, '');
this.setState(state => {
return {
...state,
profileName: newName,
};
});
}
private onKeyUp(event: any) {
switch (event.key) {
case 'Enter':
this.onClickOK();
break;
case 'Esc':
case 'Escape':
this.closeDialog();
break;
default:
}
}
private showError(msg: string) {
if (this.state.errorDisplayed) {
return;
}
this.setState({
errorDisplayed: true,
errorMessage: msg,
});
setTimeout(() => {
this.setState({
errorDisplayed: false,
});
}, 3000);
}
private onClickOK() {
const newName = this.state.profileName.trim();
if (newName === '') {
this.showError(this.props.i18n('emptyProfileNameError'));
return;
}
const avatar =
this.inputEl &&
this.inputEl.current &&
this.inputEl.current.files &&
this.inputEl.current.files.length > 0
? this.inputEl.current.files[0]
: null;
this.props.onOk(newName, avatar);
this.closeDialog();
}
private closeDialog() {
window.removeEventListener('keyup', this.onKeyUp);
this.props.onClose();
}
}

@ -334,8 +334,8 @@ export class MainHeader extends React.Component<Props, any> {
onClick: onCopyPublicKey, onClick: onCopyPublicKey,
}, },
{ {
id: 'editDisplayName', id: 'editProfile',
name: i18n('editDisplayName'), name: i18n('editProfile'),
onClick: () => { onClick: () => {
trigger('onEditProfile'); trigger('onEditProfile');
}, },

Loading…
Cancel
Save