/ common / components / BalanceSidebar / AccountAddress.tsx
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);