Full-text search within conversation

This commit is contained in:
Scott Nonnenberg 2019-08-09 16:12:29 -07:00
parent 6292019d30
commit c39d5a811a
26 changed files with 697 additions and 134 deletions

View file

@ -1,13 +1,14 @@
import React from 'react';
import classNames from 'classnames';
import { debounce } from 'lodash';
import { Avatar } from './Avatar';
import { cleanSearchTerm } from '../util/cleanSearchTerm';
import { LocalizerType } from '../types/Util';
export interface Props {
export interface PropsType {
searchTerm: string;
searchConversationName?: string;
searchConversationId?: string;
// To be used as an ID
ourNumber: string;
@ -27,55 +28,73 @@ export interface Props {
search: (
query: string,
options: {
searchConversationId?: string;
regionCode: string;
ourNumber: string;
noteToSelf: string;
}
) => void;
clearConversationSearch: () => void;
clearSearch: () => void;
}
export class MainHeader extends React.Component<Props> {
private readonly updateSearchBound: (
event: React.FormEvent<HTMLInputElement>
) => void;
private readonly clearSearchBound: () => void;
private readonly handleKeyUpBound: (
event: React.KeyboardEvent<HTMLInputElement>
) => void;
private readonly setFocusBound: () => void;
export class MainHeader extends React.Component<PropsType> {
private readonly inputRef: React.RefObject<HTMLInputElement>;
private readonly debouncedSearch: (searchTerm: string) => void;
constructor(props: Props) {
constructor(props: PropsType) {
super(props);
this.updateSearchBound = this.updateSearch.bind(this);
this.clearSearchBound = this.clearSearch.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);
}
public search() {
const { searchTerm, search, i18n, ourNumber, regionCode } = this.props;
public componentDidUpdate(prevProps: PropsType) {
const { searchConversationId } = this.props;
// When user chooses to search in a given conversation we focus the field for them
if (
searchConversationId &&
searchConversationId !== prevProps.searchConversationId
) {
this.setFocus();
}
}
// tslint:disable-next-line member-ordering
public search = debounce((searchTerm: string) => {
const {
i18n,
ourNumber,
regionCode,
search,
searchConversationId,
} = this.props;
if (search) {
search(searchTerm, {
searchConversationId,
noteToSelf: i18n('noteToSelf').toLowerCase(),
ourNumber,
regionCode,
});
}
}
}, 50);
public updateSearch(event: React.FormEvent<HTMLInputElement>) {
const { updateSearchTerm, clearSearch } = this.props;
public updateSearch = (event: React.FormEvent<HTMLInputElement>) => {
const {
updateSearchTerm,
clearConversationSearch,
clearSearch,
searchConversationId,
} = this.props;
const searchTerm = event.currentTarget.value;
if (!searchTerm) {
clearSearch();
if (searchConversationId) {
clearConversationSearch();
} else {
clearSearch();
}
return;
}
@ -88,47 +107,82 @@ export class MainHeader extends React.Component<Props> {
return;
}
const cleanedTerm = cleanSearchTerm(searchTerm);
if (!cleanedTerm) {
return;
}
this.search(searchTerm);
};
this.debouncedSearch(cleanedTerm);
}
public clearSearch() {
public clearSearch = () => {
const { clearSearch } = this.props;
clearSearch();
this.setFocus();
}
};
public handleKeyUp(event: React.KeyboardEvent<HTMLInputElement>) {
const { clearSearch } = this.props;
public clearConversationSearch = () => {
const { clearConversationSearch } = this.props;
if (event.key === 'Escape') {
clearConversationSearch();
this.setFocus();
};
public handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
const {
clearConversationSearch,
clearSearch,
searchConversationId,
searchTerm,
} = this.props;
if (event.key !== 'Escape') {
return;
}
if (searchConversationId && searchTerm) {
clearConversationSearch();
} else {
clearSearch();
}
}
};
public setFocus() {
public handleXButton = () => {
const {
searchConversationId,
clearConversationSearch,
clearSearch,
} = this.props;
if (searchConversationId) {
clearConversationSearch();
} else {
clearSearch();
}
this.setFocus();
};
public setFocus = () => {
if (this.inputRef.current) {
// @ts-ignore
this.inputRef.current.focus();
}
}
};
public render() {
const {
searchTerm,
avatarPath,
i18n,
color,
i18n,
name,
phoneNumber,
profileName,
searchConversationId,
searchConversationName,
searchTerm,
} = this.props;
const placeholder = searchConversationName
? i18n('searchIn', [searchConversationName])
: i18n('search');
return (
<div className="module-main-header">
<Avatar
@ -142,26 +196,42 @@ export class MainHeader extends React.Component<Props> {
size={28}
/>
<div className="module-main-header__search">
<div
role="button"
className="module-main-header__search__icon"
onClick={this.setFocusBound}
/>
{searchConversationId ? (
<div className="module-main-header__search__in-conversation-pill">
<div className="module-main-header__search__in-conversation-pill__avatar-container">
<div className="module-main-header__search__in-conversation-pill__avatar" />
</div>
<button
className="module-main-header__search__in-conversation-pill__x-button"
onClick={this.clearSearch}
/>
</div>
) : (
<button
className="module-main-header__search__icon"
onClick={this.setFocus}
/>
)}
<input
type="text"
ref={this.inputRef}
className="module-main-header__search__input"
placeholder={i18n('search')}
className={classNames(
'module-main-header__search__input',
searchConversationId
? 'module-main-header__search__input--in-conversation'
: null
)}
placeholder={placeholder}
dir="auto"
onKeyUp={this.handleKeyUpBound}
onKeyUp={this.handleKeyUp}
value={searchTerm}
onChange={this.updateSearchBound}
onChange={this.updateSearch}
/>
{searchTerm ? (
<div
role="button"
className="module-main-header__search__cancel-icon"
onClick={this.clearSearchBound}
onClick={this.handleXButton}
/>
) : null}
</div>