import React, { useEffect, useState } from 'react'; import classNames from 'classnames'; import { QRCode } from 'react-qr-svg'; import { Avatar, AvatarSize } from './Avatar'; import { SessionButton, SessionButtonColor, SessionButtonType } from './session/SessionButton'; import { SessionIconButton, SessionIconSize, SessionIconType } from './session/icon'; import { PillDivider } from './session/PillDivider'; import { SyncUtils, ToastUtils, UserUtils } from '../session/utils'; import { DefaultTheme } from 'styled-components'; import { MAX_USERNAME_LENGTH } from './session/registration/RegistrationTabs'; import { SessionSpinner } from './session/SessionSpinner'; import { ConversationTypeEnum } from '../models/conversation'; import { SessionWrapperModal } from './session/SessionWrapperModal'; import { AttachmentUtil } from '../util'; import { ConversationController } from '../session/conversations'; interface Props { profileName?: string; avatarPath?: string; pubkey?: string; onClose?: any; onOk?: any; theme: DefaultTheme; } interface State { profileName: string; setProfileName: string; avatar: string; mode: 'default' | 'edit' | 'qr'; loading: boolean; } export class EditProfileDialog extends React.Component { 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.onKeyUp = this.onKeyUp.bind(this); this.onFileSelected = this.onFileSelected.bind(this); this.fireInputEvent = this.fireInputEvent.bind(this); this.state = { profileName: this.props.profileName || '', setProfileName: this.props.profileName || '', avatar: this.props.avatarPath || '', mode: 'default', loading: false, }; this.inputEl = React.createRef(); window.addEventListener('keyup', this.onKeyUp); } public async componentDidMount() { const ourNumber = window.storage.get('primaryDevicePubKey'); const conversation = await ConversationController.getInstance().getOrCreateAndWait( ourNumber, ConversationTypeEnum.PRIVATE ); const readFile = async (attachment: any) => new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = (e: any) => { 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 displayName = profile && profile.displayName; this.setState({ ...this.state, profileName: profile.profileName, avatar: avatarPath || '', setProfileName: profile.profileName, }); } public render() { // const i18n = this.props.i18n; const i18n = window.i18n; const viewDefault = this.state.mode === 'default'; const viewEdit = this.state.mode === 'edit'; const viewQR = this.state.mode === 'qr'; const sessionID = UserUtils.getOurPubKeyStrFromCache(); const backButton = viewEdit || viewQR ? [ { iconType: SessionIconType.Chevron, iconRotation: 90, onClick: () => { this.setState({ mode: 'default' }); }, }, ] : undefined; return (
{viewQR && this.renderQRView(sessionID)} {viewDefault && this.renderDefaultView()} {viewEdit && this.renderEditView()}

{sessionID}

{viewDefault || viewQR ? ( { this.copySessionID(sessionID); }} /> ) : ( !this.state.loading && ( ) )}
); } private renderProfileHeader() { return ( <>
{this.renderAvatar()}
{ this.setState({ mode: 'qr' }); }} theme={this.props.theme} />
); } private fireInputEvent() { this.setState({ mode: 'edit' }, () => { const el = this.inputEl.current; if (el) { el.click(); } }); } private renderDefaultView() { const name = this.state.setProfileName ? this.state.setProfileName : this.state.profileName; return ( <> {this.renderProfileHeader()}

{name}

{ this.setState({ mode: 'edit' }); }} theme={this.props.theme} />
); } private renderEditView() { const placeholderText = window.i18n('displayName'); return ( <> {this.renderProfileHeader()}
); } private renderQRView(sessionID: string) { const bgColor = '#FFFFFF'; const fgColor = '#1B1B1B'; return (
); } private onFileSelected() { const file = this.inputEl.current.files[0]; const url = window.URL.createObjectURL(file); this.setState({ avatar: url, }); } private renderAvatar() { const { avatar, profileName } = this.state; const { pubkey } = this.props; const userName = profileName || pubkey; return ; } private onNameEdited(event: any) { const newName = event.target.value.replace(window.displayNameRegex, ''); this.setState(state => { return { ...state, profileName: newName, }; }); } private onKeyUp(event: any) { switch (event.key) { case 'Enter': if (this.state.mode === 'edit') { // this.onClickOK(); this.onClickOK(); } break; case 'Esc': case 'Escape': this.closeDialog(); break; default: } } private copySessionID(sessionID: string) { window.clipboard.writeText(sessionID); ToastUtils.pushCopiedToClipBoard(); } /** * Tidy the profile name input text and save the new profile name and avatar */ private onClickOK() { const newName = this.state.profileName ? this.state.profileName.trim() : ''; if (newName.length === 0 || newName.length > MAX_USERNAME_LENGTH) { 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.setState( { loading: true, }, async () => { await this.commitProfileEdits(newName, avatar); this.setState({ loading: false, mode: 'default', setProfileName: this.state.profileName, }); } ); } private closeDialog() { window.removeEventListener('keyup', this.onKeyUp); this.props.onClose(); } private async commitProfileEdits(newName: string, avatar: any) { const ourNumber = window.storage.get('primaryDevicePubKey'); const conversation = await ConversationController.getInstance().getOrCreateAndWait( ourNumber, ConversationTypeEnum.PRIVATE ); let newAvatarPath = ''; let url: any = null; let profileKey: any = null; if (avatar) { const data = await AttachmentUtil.readFile({ file: avatar }); // Ensure that this file is either small enough or is resized to meet our // requirements for attachments try { const withBlob = await AttachmentUtil.autoScale( { contentType: avatar.type, file: new Blob([data.data], { type: avatar.contentType, }), }, { maxSide: 640, maxSize: 1000 * 1024, } ); const dataResized = await window.Signal.Types.Attachment.arrayBufferFromFile(withBlob.file); // 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); await conversation.setLokiProfile({ displayName: newName }); conversation.set('avatar', tempUrl); // Encrypt with a new key every time profileKey = window.libsignal.crypto.getRandomBytes(32); const encryptedData = await window.textsecure.crypto.encryptProfile( dataResized, profileKey ); throw new Error('uploadAvatarV1 to move to v2'); // const avatarPointer = await AttachmentUtils.uploadAvatarV1({ // ...dataResized, // data: encryptedData, // size: encryptedData.byteLength, // }); // url = avatarPointer ? avatarPointer.url : null; // window.storage.put('profileKey', profileKey); // conversation.set('avatarPointer', url); // const upgraded = await window.Signal.Migrations.processNewAttachment({ // isRaw: true, // data: data.data, // url, // }); // newAvatarPath = upgraded.path; // // Replace our temporary image with the attachment pointer from the server: // conversation.set('avatar', null); // await conversation.setLokiProfile({ // displayName: newName, // avatar: newAvatarPath, // }); // await conversation.commit(); // UserUtils.setLastProfileUpdateTimestamp(Date.now()); // await SyncUtils.forceSyncConfigurationNowIfNeeded(true); } catch (error) { window.log.error( 'showEditProfileDialog Error ensuring that image is properly sized:', error && error.stack ? error.stack : error ); } } else { // do not update the avatar if it did not change await conversation.setLokiProfile({ displayName: newName, }); // might be good to not trigger a sync if the name did not change await conversation.commit(); UserUtils.setLastProfileUpdateTimestamp(Date.now()); await SyncUtils.forceSyncConfigurationNowIfNeeded(true); } if (avatar) { ConversationController.getInstance() .getConversations() .filter(convo => convo.isPublic()) .forEach(convo => convo.trigger('ourAvatarChanged', { url, profileKey })); } } }