AccountAddress.tsx
1 import React from 'react'; 2 import { connect, MapStateToProps } from 'react-redux'; 3 import { CopyToClipboard } from 'react-copy-to-clipboard'; 4 5 import translate, { translateRaw } from 'translations'; 6 import { AppState } from 'features/reducers'; 7 import { 8 addressBookConstants, 9 addressBookActions, 10 addressBookSelectors 11 } from 'features/addressBook'; 12 import { Address, Identicon, Input } from 'components/ui'; 13 14 interface StateProps { 15 entry: ReturnType<typeof addressBookSelectors.getAccountAddressEntry>; 16 addressLabel: string; 17 } 18 19 interface DispatchProps { 20 changeAddressLabelEntry: addressBookActions.TChangeAddressLabelEntry; 21 saveAddressLabelEntry: addressBookActions.TSaveAddressLabelEntry; 22 removeAddressLabelEntry: addressBookActions.TRemoveAddressLabelEntry; 23 } 24 25 interface OwnProps { 26 address: string; 27 } 28 29 type Props = StateProps & DispatchProps & OwnProps; 30 31 interface State { 32 copied: boolean; 33 editingLabel: boolean; 34 labelInputTouched: boolean; 35 } 36 37 class AccountAddress extends React.Component<Props, State> { 38 public state = { 39 copied: false, 40 editingLabel: false, 41 labelInputTouched: false 42 }; 43 44 private goingToClearCopied: number | null = null; 45 46 private labelInput: HTMLInputElement | null = null; 47 48 public handleCopy = () => 49 this.setState( 50 (prevState: State) => ({ 51 copied: !prevState.copied 52 }), 53 this.clearCopied 54 ); 55 56 public componentWillUnmount() { 57 if (this.goingToClearCopied) { 58 window.clearTimeout(this.goingToClearCopied); 59 } 60 } 61 62 public render() { 63 const { address, addressLabel } = this.props; 64 const { copied } = this.state; 65 const labelContent = this.generateLabelContent(); 66 const labelButton = this.generateLabelButton(); 67 const addressClassName = `AccountInfo-address-addr ${ 68 addressLabel ? 'AccountInfo-address-addr--small' : '' 69 }`; 70 71 return ( 72 <div className="AccountInfo"> 73 <h5 className="AccountInfo-section-header">{translate('SIDEBAR_ACCOUNTADDR')}</h5> 74 <div className="AccountInfo-section AccountInfo-address-section"> 75 <div className="AccountInfo-address-icon"> 76 <Identicon address={address} size="100%" /> 77 </div> 78 <div className="AccountInfo-address-wrapper"> 79 {labelContent} 80 <div className={addressClassName}> 81 <Address address={address} /> 82 </div> 83 <CopyToClipboard onCopy={this.handleCopy} text={address}> 84 <div 85 className={`AccountInfo-copy ${copied ? 'is-copied' : ''}`} 86 title={translateRaw('COPY_TO_CLIPBOARD')} 87 > 88 <i className="fa fa-copy" /> 89 <span>{translateRaw(copied ? 'COPIED' : 'COPY_ADDRESS')}</span> 90 </div> 91 </CopyToClipboard> 92 <div className="AccountInfo-label" title={translateRaw('EDIT_LABEL_2')}> 93 {labelButton} 94 </div> 95 </div> 96 </div> 97 </div> 98 ); 99 } 100 101 private clearCopied = () => 102 (this.goingToClearCopied = window.setTimeout(() => this.setState({ copied: false }), 2000)); 103 104 private startEditingLabel = () => 105 this.setState({ editingLabel: true }, () => { 106 if (this.labelInput) { 107 this.labelInput.focus(); 108 this.labelInput.select(); 109 } 110 }); 111 112 private stopEditingLabel = () => this.setState({ editingLabel: false }); 113 114 private setLabelInputRef = (node: HTMLInputElement) => (this.labelInput = node); 115 116 private generateLabelContent = () => { 117 const { addressLabel, entry: { temporaryLabel, labelError } } = this.props; 118 const { editingLabel, labelInputTouched } = this.state; 119 const newLabelSameAsPrevious = temporaryLabel === addressLabel; 120 const labelInputTouchedWithError = labelInputTouched && !newLabelSameAsPrevious && labelError; 121 122 let labelContent = null; 123 124 if (editingLabel) { 125 labelContent = ( 126 <React.Fragment> 127 <Input 128 title={translateRaw('ADD_LABEL')} 129 placeholder={translateRaw('NEW_LABEL')} 130 defaultValue={addressLabel} 131 onChange={this.handleLabelChange} 132 onKeyDown={this.handleKeyDown} 133 onFocus={this.setTemporaryLabelTouched} 134 onBlur={this.handleBlur} 135 showInvalidBeforeBlur={true} 136 setInnerRef={this.setLabelInputRef} 137 isValid={!labelInputTouchedWithError} 138 /> 139 {labelInputTouchedWithError && ( 140 <label className="AccountInfo-address-wrapper-error">{labelError}</label> 141 )} 142 </React.Fragment> 143 ); 144 } else { 145 labelContent = <label className="AccountInfo-address-label">{addressLabel}</label>; 146 } 147 148 return labelContent; 149 }; 150 151 private generateLabelButton = () => { 152 const { addressLabel } = this.props; 153 const { editingLabel } = this.state; 154 const labelButton = editingLabel ? ( 155 <React.Fragment> 156 <i className="fa fa-save" /> 157 <span role="button" title={translateRaw('SAVE_LABEL')} onClick={this.stopEditingLabel}> 158 {translate('SAVE_LABEL')} 159 </span> 160 </React.Fragment> 161 ) : ( 162 <React.Fragment> 163 <i className="fa fa-pencil" /> 164 <span 165 role="button" 166 title={addressLabel ? translateRaw('EDIT_LABEL') : translateRaw('ADD_LABEL_9')} 167 onClick={this.startEditingLabel} 168 > 169 {addressLabel ? translate('EDIT_LABEL') : translate('ADD_LABEL_9')} 170 </span> 171 </React.Fragment> 172 ); 173 174 return labelButton; 175 }; 176 177 private handleBlur = () => { 178 const { address, addressLabel, entry: { id, label, temporaryLabel, labelError } } = this.props; 179 180 this.clearTemporaryLabelTouched(); 181 this.stopEditingLabel(); 182 183 if (temporaryLabel === addressLabel) { 184 return; 185 } 186 187 if (temporaryLabel && temporaryLabel.length > 0) { 188 this.props.saveAddressLabelEntry(id); 189 190 if (labelError) { 191 // If the new changes aren't valid, undo them. 192 this.props.changeAddressLabelEntry({ 193 id, 194 address, 195 temporaryAddress: address, 196 label, 197 temporaryLabel: label, 198 overrideValidation: true 199 }); 200 } 201 } else { 202 this.props.removeAddressLabelEntry(id); 203 } 204 }; 205 206 private handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { 207 switch (e.key) { 208 case 'Enter': 209 return this.handleBlur(); 210 case 'Escape': 211 return this.stopEditingLabel(); 212 } 213 }; 214 215 private handleLabelChange = (e: React.ChangeEvent<HTMLInputElement>) => { 216 const { address } = this.props; 217 const label = e.target.value; 218 219 this.props.changeAddressLabelEntry({ 220 id: addressBookConstants.ACCOUNT_ADDRESS_ID, 221 address, 222 label, 223 isEditing: true 224 }); 225 226 this.setState( 227 { 228 labelInputTouched: true 229 }, 230 () => label.length === 0 && this.clearTemporaryLabelTouched() 231 ); 232 }; 233 234 private setTemporaryLabelTouched = () => { 235 const { labelInputTouched } = this.state; 236 237 if (!labelInputTouched) { 238 this.setState({ labelInputTouched: true }); 239 } 240 }; 241 242 private clearTemporaryLabelTouched = () => this.setState({ labelInputTouched: false }); 243 } 244 245 const mapStateToProps: MapStateToProps<StateProps, {}, AppState> = ( 246 state: AppState, 247 ownProps: OwnProps 248 ) => { 249 const labelEntry = addressBookSelectors.getAddressLabelEntryFromAddress(state, ownProps.address); 250 return { 251 entry: addressBookSelectors.getAccountAddressEntry(state), 252 addressLabel: labelEntry ? labelEntry.label : '' 253 }; 254 }; 255 256 const mapDispatchToProps: DispatchProps = { 257 changeAddressLabelEntry: addressBookActions.changeAddressLabelEntry, 258 saveAddressLabelEntry: addressBookActions.saveAddressLabelEntry, 259 removeAddressLabelEntry: addressBookActions.removeAddressLabelEntry 260 }; 261 262 export default connect<StateProps, DispatchProps, OwnProps, AppState>( 263 mapStateToProps, 264 mapDispatchToProps 265 )(AccountAddress);