You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			420 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			420 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			TypeScript
		
	
import React from 'react';
 | 
						|
import { debounce } from 'lodash';
 | 
						|
import classNames from 'classnames';
 | 
						|
 | 
						|
// Use this to trigger whisper events
 | 
						|
import { trigger } from '../shims/events';
 | 
						|
 | 
						|
// Use this to check for password
 | 
						|
import { hasPassword } from '../shims/Signal';
 | 
						|
 | 
						|
import { Avatar } from './Avatar';
 | 
						|
import { ContactName } from './conversation/ContactName';
 | 
						|
 | 
						|
import { cleanSearchTerm } from '../util/cleanSearchTerm';
 | 
						|
import { LocalizerType } from '../types/Util';
 | 
						|
import { SearchOptions } from '../types/Search';
 | 
						|
import { clipboard } from 'electron';
 | 
						|
 | 
						|
import { validateNumber } from '../types/PhoneNumber';
 | 
						|
 | 
						|
declare global {
 | 
						|
  interface Window {
 | 
						|
    lokiFeatureFlags: any;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
interface MenuItem {
 | 
						|
  id: string;
 | 
						|
  name: string;
 | 
						|
  onClick?: () => void;
 | 
						|
}
 | 
						|
export interface Props {
 | 
						|
  searchTerm: string;
 | 
						|
 | 
						|
  // To be used as an ID
 | 
						|
  ourNumber: string;
 | 
						|
  regionCode: string;
 | 
						|
 | 
						|
  // For display
 | 
						|
  phoneNumber: string;
 | 
						|
  isMe: boolean;
 | 
						|
  name?: string;
 | 
						|
  color: string;
 | 
						|
  verified: boolean;
 | 
						|
  profileName?: string;
 | 
						|
  avatarPath?: string;
 | 
						|
  isSecondaryDevice: boolean;
 | 
						|
 | 
						|
  i18n: LocalizerType;
 | 
						|
  updateSearchTerm: (searchTerm: string) => void;
 | 
						|
  search: (query: string, options: SearchOptions) => void;
 | 
						|
  clearSearch: () => void;
 | 
						|
 | 
						|
  onClick?: () => void;
 | 
						|
  onCopyPublicKey?: () => void;
 | 
						|
}
 | 
						|
 | 
						|
export class MainHeader extends React.Component<Props, any> {
 | 
						|
  private readonly updateSearchBound: (
 | 
						|
    event: React.FormEvent<HTMLInputElement>
 | 
						|
  ) => void;
 | 
						|
  private readonly clearSearchBound: () => void;
 | 
						|
  private readonly copyFromClipboardBound: () => void;
 | 
						|
  private readonly handleKeyUpBound: (
 | 
						|
    event: React.KeyboardEvent<HTMLInputElement>
 | 
						|
  ) => void;
 | 
						|
  private readonly setFocusBound: () => void;
 | 
						|
  private readonly inputRef: React.RefObject<HTMLInputElement>;
 | 
						|
  private readonly debouncedSearch: (searchTerm: string) => void;
 | 
						|
  private readonly timerId: any;
 | 
						|
 | 
						|
  constructor(props: Props) {
 | 
						|
    super(props);
 | 
						|
 | 
						|
    this.state = {
 | 
						|
      expanded: false,
 | 
						|
      hasPass: null,
 | 
						|
      clipboardText: '',
 | 
						|
      menuItems: [],
 | 
						|
    };
 | 
						|
 | 
						|
    this.updateSearchBound = this.updateSearch.bind(this);
 | 
						|
    this.clearSearchBound = this.clearSearch.bind(this);
 | 
						|
    this.copyFromClipboardBound = this.copyFromClipboard.bind(this);
 | 
						|
    this.handleKeyUpBound = this.handleKeyUp.bind(this);
 | 
						|
    this.setFocusBound = this.setFocus.bind(this);
 | 
						|
    this.inputRef = React.createRef();
 | 
						|
 | 
						|
    this.debouncedSearch = debounce(this.search.bind(this), 20);
 | 
						|
 | 
						|
    this.timerId = setInterval(() => {
 | 
						|
      const clipboardText = clipboard.readText();
 | 
						|
      if (this.state.clipboardText !== clipboardText) {
 | 
						|
        this.setState({ clipboardText });
 | 
						|
      }
 | 
						|
    }, 100);
 | 
						|
  }
 | 
						|
 | 
						|
  public componentWillMount() {
 | 
						|
    // tslint:disable-next-line
 | 
						|
    this.updateHasPass();
 | 
						|
  }
 | 
						|
 | 
						|
  public componentWillUnmount() {
 | 
						|
    // tslint:disable-next-line
 | 
						|
    clearInterval(this.timerId);
 | 
						|
  }
 | 
						|
 | 
						|
  public componentDidUpdate(_prevProps: Props, prevState: any) {
 | 
						|
    if (
 | 
						|
      prevState.hasPass !== this.state.hasPass ||
 | 
						|
      _prevProps.isSecondaryDevice !== this.props.isSecondaryDevice
 | 
						|
    ) {
 | 
						|
      this.updateMenuItems();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  public search() {
 | 
						|
    const {
 | 
						|
      searchTerm,
 | 
						|
      search,
 | 
						|
      i18n,
 | 
						|
      ourNumber,
 | 
						|
      regionCode,
 | 
						|
      isSecondaryDevice,
 | 
						|
    } = this.props;
 | 
						|
    if (search) {
 | 
						|
      search(searchTerm, {
 | 
						|
        noteToSelf: i18n('noteToSelf').toLowerCase(),
 | 
						|
        ourNumber,
 | 
						|
        regionCode,
 | 
						|
        isSecondaryDevice,
 | 
						|
      });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  public updateSearch(event: React.FormEvent<HTMLInputElement>) {
 | 
						|
    const { updateSearchTerm, clearSearch } = this.props;
 | 
						|
    const searchTerm = event.currentTarget.value;
 | 
						|
 | 
						|
    if (!searchTerm) {
 | 
						|
      clearSearch();
 | 
						|
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (updateSearchTerm) {
 | 
						|
      updateSearchTerm(searchTerm);
 | 
						|
    }
 | 
						|
 | 
						|
    if (searchTerm.length < 2) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    const cleanedTerm = cleanSearchTerm(searchTerm);
 | 
						|
    if (!cleanedTerm) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.debouncedSearch(cleanedTerm);
 | 
						|
  }
 | 
						|
 | 
						|
  public clearSearch() {
 | 
						|
    const { clearSearch } = this.props;
 | 
						|
 | 
						|
    clearSearch();
 | 
						|
    this.setFocus();
 | 
						|
  }
 | 
						|
 | 
						|
  public copyFromClipboard() {
 | 
						|
    const { clipboardText } = this.state;
 | 
						|
 | 
						|
    this.props.updateSearchTerm(clipboardText);
 | 
						|
    this.debouncedSearch(clipboardText);
 | 
						|
  }
 | 
						|
 | 
						|
  public handleKeyUp(event: React.KeyboardEvent<HTMLInputElement>) {
 | 
						|
    const { clearSearch } = this.props;
 | 
						|
 | 
						|
    if (event.key === 'Escape') {
 | 
						|
      clearSearch();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  public setFocus() {
 | 
						|
    if (this.inputRef.current) {
 | 
						|
      // @ts-ignore
 | 
						|
      this.inputRef.current.focus();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  public render() {
 | 
						|
    const { onClick } = this.props;
 | 
						|
 | 
						|
    return (
 | 
						|
      <div role="button" className="module-main-header" onClick={onClick}>
 | 
						|
        <div className="module-main-header__container">
 | 
						|
          {this.renderName()}
 | 
						|
          {this.renderMenu()}
 | 
						|
        </div>
 | 
						|
        {this.renderSearch()}
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  private renderName() {
 | 
						|
    const {
 | 
						|
      avatarPath,
 | 
						|
      i18n,
 | 
						|
      color,
 | 
						|
      name,
 | 
						|
      phoneNumber,
 | 
						|
      profileName,
 | 
						|
    } = this.props;
 | 
						|
 | 
						|
    const { expanded } = this.state;
 | 
						|
 | 
						|
    return (
 | 
						|
      <div
 | 
						|
        role="button"
 | 
						|
        className="module-main-header__title"
 | 
						|
        onClick={() => {
 | 
						|
          this.setState({ expanded: !expanded });
 | 
						|
        }}
 | 
						|
      >
 | 
						|
        <Avatar
 | 
						|
          avatarPath={avatarPath}
 | 
						|
          color={color}
 | 
						|
          conversationType="direct"
 | 
						|
          i18n={i18n}
 | 
						|
          name={name}
 | 
						|
          phoneNumber={phoneNumber}
 | 
						|
          profileName={profileName}
 | 
						|
          size={28}
 | 
						|
        />
 | 
						|
        <div className="module-main-header__contact-name">
 | 
						|
          <ContactName
 | 
						|
            phoneNumber={phoneNumber}
 | 
						|
            profileName={profileName}
 | 
						|
            i18n={i18n}
 | 
						|
          />
 | 
						|
        </div>
 | 
						|
        <div
 | 
						|
          className={classNames(
 | 
						|
            'module-main-header-content-toggle',
 | 
						|
            expanded && 'module-main-header-content-toggle-visible'
 | 
						|
          )}
 | 
						|
        />
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  private renderMenu() {
 | 
						|
    const { expanded, menuItems } = this.state;
 | 
						|
 | 
						|
    return (
 | 
						|
      <div className="module-main-header__menu">
 | 
						|
        <div className={classNames('accordian', expanded && 'expanded')}>
 | 
						|
          {menuItems.map((item: MenuItem) => (
 | 
						|
            <div
 | 
						|
              role="button"
 | 
						|
              className="menu-item"
 | 
						|
              key={item.id}
 | 
						|
              onClick={item.onClick}
 | 
						|
            >
 | 
						|
              {item.name}
 | 
						|
            </div>
 | 
						|
          ))}
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  private shouldShowPaste() {
 | 
						|
    const { searchTerm, i18n } = this.props;
 | 
						|
    const { clipboardText } = this.state;
 | 
						|
 | 
						|
    const error = validateNumber(clipboardText, i18n);
 | 
						|
 | 
						|
    return !searchTerm && !error;
 | 
						|
  }
 | 
						|
 | 
						|
  private renderSearch() {
 | 
						|
    const { searchTerm, i18n } = this.props;
 | 
						|
 | 
						|
    return (
 | 
						|
      <div className="module-main-header__search">
 | 
						|
        <input
 | 
						|
          type="text"
 | 
						|
          ref={this.inputRef}
 | 
						|
          className="module-main-header__search__input"
 | 
						|
          placeholder={i18n('search')}
 | 
						|
          dir="auto"
 | 
						|
          onKeyUp={this.handleKeyUpBound}
 | 
						|
          value={searchTerm}
 | 
						|
          onChange={this.updateSearchBound}
 | 
						|
        />
 | 
						|
        {this.shouldShowPaste() ? (
 | 
						|
          <span
 | 
						|
            role="button"
 | 
						|
            className="module-main-header__search__copy-from-clipboard"
 | 
						|
            onClick={this.copyFromClipboardBound}
 | 
						|
          />
 | 
						|
        ) : null}
 | 
						|
        <span
 | 
						|
          role="button"
 | 
						|
          className="module-main-header__search__icon"
 | 
						|
          onClick={this.setFocusBound}
 | 
						|
        />
 | 
						|
        {searchTerm ? (
 | 
						|
          <span
 | 
						|
            role="button"
 | 
						|
            className="module-main-header__search__cancel-icon"
 | 
						|
            onClick={this.clearSearchBound}
 | 
						|
          />
 | 
						|
        ) : null}
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  private async updateHasPass() {
 | 
						|
    const hasPass = await hasPassword();
 | 
						|
    this.setState({ hasPass });
 | 
						|
  }
 | 
						|
 | 
						|
  private updateMenuItems() {
 | 
						|
    const { i18n, onCopyPublicKey, isSecondaryDevice } = this.props;
 | 
						|
    const { hasPass } = this.state;
 | 
						|
 | 
						|
    const menuItems = [
 | 
						|
      {
 | 
						|
        id: 'copyPublicKey',
 | 
						|
        name: i18n('copyPublicKey'),
 | 
						|
        onClick: onCopyPublicKey,
 | 
						|
      },
 | 
						|
      {
 | 
						|
        id: 'showSeed',
 | 
						|
        name: i18n('showSeed'),
 | 
						|
        onClick: () => {
 | 
						|
          trigger('showSeedDialog');
 | 
						|
        },
 | 
						|
      },
 | 
						|
      {
 | 
						|
        id: 'showQRCode',
 | 
						|
        name: i18n('showQRCode'),
 | 
						|
        onClick: () => {
 | 
						|
          trigger('showQRDialog');
 | 
						|
        },
 | 
						|
      },
 | 
						|
      {
 | 
						|
        id: 'showAddServer',
 | 
						|
        name: i18n('showAddServer'),
 | 
						|
        onClick: () => {
 | 
						|
          trigger('showAddServerDialog');
 | 
						|
        },
 | 
						|
      },
 | 
						|
    ];
 | 
						|
 | 
						|
    if (window.lokiFeatureFlags.privateGroupChats) {
 | 
						|
      menuItems.push({
 | 
						|
        id: 'createPrivateGroup',
 | 
						|
        name: i18n('createPrivateGroup'),
 | 
						|
        onClick: () => {
 | 
						|
          trigger('createNewGroup');
 | 
						|
        },
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    const passItem = (type: string) => ({
 | 
						|
      id: `${type}Password`,
 | 
						|
      name: i18n(`${type}Password`),
 | 
						|
      onClick: () => {
 | 
						|
        trigger('showPasswordDialog', {
 | 
						|
          type,
 | 
						|
          resolve: () => {
 | 
						|
            trigger('showToast', {
 | 
						|
              message: i18n(`${type}PasswordSuccess`),
 | 
						|
            });
 | 
						|
            setTimeout(async () => this.updateHasPass(), 100);
 | 
						|
          },
 | 
						|
        });
 | 
						|
      },
 | 
						|
    });
 | 
						|
 | 
						|
    if (hasPass) {
 | 
						|
      menuItems.push(passItem('change'), passItem('remove'));
 | 
						|
    } else {
 | 
						|
      menuItems.push(passItem('set'));
 | 
						|
    }
 | 
						|
 | 
						|
    if (!isSecondaryDevice) {
 | 
						|
      // insert as second element
 | 
						|
      menuItems.splice(1, 0, {
 | 
						|
        id: 'editProfile',
 | 
						|
        name: i18n('editProfile'),
 | 
						|
        onClick: () => {
 | 
						|
          trigger('onEditProfile');
 | 
						|
        },
 | 
						|
      });
 | 
						|
      menuItems.push({
 | 
						|
        id: 'pairNewDevice',
 | 
						|
        name: 'Device Pairing',
 | 
						|
        onClick: () => {
 | 
						|
          trigger('showDevicePairingDialog');
 | 
						|
        },
 | 
						|
      });
 | 
						|
    } else {
 | 
						|
      menuItems.push({
 | 
						|
        id: 'showPairingWords',
 | 
						|
        name: 'Show Pairing Words',
 | 
						|
        onClick: () => {
 | 
						|
          trigger('showDevicePairingWordsDialog');
 | 
						|
        },
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    this.setState({ menuItems });
 | 
						|
  }
 | 
						|
}
 |