Full-text search within conversation
This commit is contained in:
@@ -418,3 +418,38 @@ const conversations = [
|
||||
/>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
||||
#### Searching in conversation
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||
<LeftPane
|
||||
searchResults={{
|
||||
searchConversationName: "Y'all 🌆",
|
||||
}}
|
||||
conversations={[]}
|
||||
archivedConversations={[]}
|
||||
showArchived={false}
|
||||
startNewConversation={(query, options) =>
|
||||
console.log('startNewConversation', query, options)
|
||||
}
|
||||
openConversationInternal={(id, messageId) =>
|
||||
console.log('openConversation', id, messageId)
|
||||
}
|
||||
showArchivedConversations={() => console.log('showArchivedConversations')}
|
||||
showInbox={() => console.log('showInbox')}
|
||||
renderMainHeader={() => (
|
||||
<MainHeader
|
||||
searchTerm=""
|
||||
search={result => console.log('search', result)}
|
||||
searchConversationName="Y'all 🌆"
|
||||
searchConversationId="group-id-1"
|
||||
updateSearch={result => console.log('updateSearch', result)}
|
||||
clearSearch={result => console.log('clearSearch', result)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
@@ -63,3 +63,38 @@ if the parent of this component feeds the updated `searchTerm` back.
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### Searching within conversation
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
<MainHeader
|
||||
name="John Smith"
|
||||
color="purple"
|
||||
searchConversationId="group-id-1"
|
||||
searchConversationName="Everyone 🔥"
|
||||
search={(...args) => console.log('search', args)}
|
||||
updateSearchTerm={(...args) => console.log('updateSearchTerm', args)}
|
||||
clearSearch={(...args) => console.log('clearSearch', args)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### Searching within conversation, with search term
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
<MainHeader
|
||||
name="John Smith"
|
||||
color="purple"
|
||||
searchConversationId="group-id-1"
|
||||
searchConversationName="Everyone 🔥"
|
||||
searchTerm="address"
|
||||
search={(...args) => console.log('search', args)}
|
||||
updateSearchTerm={(...args) => console.log('updateSearchTerm', args)}
|
||||
clearSearch={(...args) => console.log('clearSearch', args)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
@@ -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>
|
||||
|
@@ -82,6 +82,47 @@
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### Searching within conversation
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
<MessageSearchResult
|
||||
isSearchingInConversation={true}
|
||||
from={{
|
||||
name: 'Someone 🔥',
|
||||
phoneNumber: '(202) 555-0011',
|
||||
avatarPath: util.gifObjectUrl,
|
||||
}}
|
||||
to={{
|
||||
name: 'Everyone 🔥',
|
||||
}}
|
||||
snippet="What's <<left>>going<<right>> on?"
|
||||
id="messageId1"
|
||||
conversationId="conversationId1"
|
||||
receivedAt={Date.now() - 3 * 60 * 1000}
|
||||
onClick={result => console.log('onClick', result)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<MessageSearchResult
|
||||
isSearchingInConversation={true}
|
||||
from={{
|
||||
name: 'Someone 🔥',
|
||||
phoneNumber: '(202) 555-0011',
|
||||
avatarPath: util.gifObjectUrl,
|
||||
}}
|
||||
to={{
|
||||
name: 'Everyone 🔥',
|
||||
}}
|
||||
snippet="How is everyone? <<left>>Going<<right>> well?"
|
||||
id="messageId2"
|
||||
conversationId="conversationId2"
|
||||
receivedAt={Date.now() - 27 * 60 * 1000}
|
||||
onClick={result => console.log('onClick', result)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### From you and to you
|
||||
|
||||
```jsx
|
||||
|
@@ -10,6 +10,7 @@ import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsDataType = {
|
||||
isSelected?: boolean;
|
||||
isSearchingInConversation?: boolean;
|
||||
|
||||
id: string;
|
||||
conversationId: string;
|
||||
@@ -75,10 +76,10 @@ export class MessageSearchResult extends React.PureComponent<PropsType> {
|
||||
}
|
||||
|
||||
public renderFrom() {
|
||||
const { i18n, to } = this.props;
|
||||
const { i18n, to, isSearchingInConversation } = this.props;
|
||||
const fromName = this.renderFromName();
|
||||
|
||||
if (!to.isMe) {
|
||||
if (!to.isMe && !isSearchingInConversation) {
|
||||
return (
|
||||
<div className="module-message-search-result__header__from">
|
||||
{fromName} {i18n('to')}{' '}
|
||||
|
@@ -727,6 +727,76 @@ const items = [
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### With no results at all, searching in conversation
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext
|
||||
theme={util.theme}
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SearchResults
|
||||
items={[]}
|
||||
noResults={true}
|
||||
searchTerm="something"
|
||||
searchInConversationName="Everyone 🔥"
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
startNewConversation={(...args) =>
|
||||
console.log('startNewConversation', args)
|
||||
}
|
||||
onStartNewConversation={(...args) =>
|
||||
console.log('onStartNewConversation', args)
|
||||
}
|
||||
renderMessageSearchResult={id => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### Searching in conversation but no search term
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext
|
||||
theme={util.theme}
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SearchResults
|
||||
items={[]}
|
||||
noResults={true}
|
||||
searchTerm=""
|
||||
searchInConversationName="Everyone 🔥"
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
startNewConversation={(...args) =>
|
||||
console.log('startNewConversation', args)
|
||||
}
|
||||
onStartNewConversation={(...args) =>
|
||||
console.log('onStartNewConversation', args)
|
||||
}
|
||||
renderMessageSearchResult={id => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### With a lot of results
|
||||
|
||||
```jsx
|
||||
|
@@ -6,6 +6,8 @@ import {
|
||||
List,
|
||||
} from 'react-virtualized';
|
||||
|
||||
import { Intl } from './Intl';
|
||||
import { Emojify } from './conversation/Emojify';
|
||||
import {
|
||||
ConversationListItem,
|
||||
PropsData as ConversationListItemPropsType,
|
||||
@@ -19,6 +21,7 @@ export type PropsDataType = {
|
||||
noResults: boolean;
|
||||
regionCode: string;
|
||||
searchTerm: string;
|
||||
searchConversationName?: string;
|
||||
};
|
||||
|
||||
type StartNewConversationType = {
|
||||
@@ -237,14 +240,33 @@ export class SearchResults extends React.Component<PropsType> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { items, i18n, noResults, searchTerm } = this.props;
|
||||
const {
|
||||
i18n,
|
||||
items,
|
||||
noResults,
|
||||
searchConversationName,
|
||||
searchTerm,
|
||||
} = this.props;
|
||||
|
||||
if (noResults) {
|
||||
return (
|
||||
<div className="module-search-results">
|
||||
<div className="module-search-results__no-results">
|
||||
{i18n('noSearchResults', [searchTerm])}
|
||||
</div>
|
||||
{!searchConversationName || searchTerm ? (
|
||||
<div className="module-search-results__no-results" key={searchTerm}>
|
||||
{searchConversationName ? (
|
||||
<Intl
|
||||
id="noSearchResultsInConversation"
|
||||
i18n={i18n}
|
||||
components={[
|
||||
searchTerm,
|
||||
<Emojify key="item-1" text={searchConversationName} />,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
i18n('noSearchResults', [searchTerm])
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
### Name variations, 1:1 conversation
|
||||
|
||||
Note the five items in gear menu, and the second-level menu with disappearing messages options. Disappearing message set to 'off'.
|
||||
Note the five items in menu, and the second-level menu with disappearing messages options. Disappearing message set to 'off'.
|
||||
|
||||
#### With name and profile, verified
|
||||
|
||||
@@ -24,6 +24,7 @@ Note the five items in gear menu, and the second-level menu with disappearing me
|
||||
onShowAllMedia={() => console.log('onShowAllMedia')}
|
||||
onShowGroupMembers={() => console.log('onShowGroupMembers')}
|
||||
onGoBack={() => console.log('onGoBack')}
|
||||
onSearchInConversation={() => console.log('onSearchInConversation')}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
@@ -37,6 +37,7 @@ interface Props {
|
||||
onSetDisappearingMessages: (seconds: number) => void;
|
||||
onDeleteMessages: () => void;
|
||||
onResetSession: () => void;
|
||||
onSearchInConversation: () => void;
|
||||
|
||||
onShowSafetyNumber: () => void;
|
||||
onShowAllMedia: () => void;
|
||||
@@ -152,14 +153,21 @@ export class ConversationHeader extends React.Component<Props> {
|
||||
}
|
||||
|
||||
public renderExpirationLength() {
|
||||
const { expirationSettingName } = this.props;
|
||||
const { expirationSettingName, showBackButton } = this.props;
|
||||
|
||||
if (!expirationSettingName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-conversation-header__expiration">
|
||||
<div
|
||||
className={classNames(
|
||||
'module-conversation-header__expiration',
|
||||
showBackButton
|
||||
? 'module-conversation-header__expiration--hidden'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
<div className="module-conversation-header__expiration__clock-icon" />
|
||||
<div className="module-conversation-header__expiration__setting">
|
||||
{expirationSettingName}
|
||||
@@ -168,7 +176,7 @@ export class ConversationHeader extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
public renderGear(triggerId: string) {
|
||||
public renderMoreButton(triggerId: string) {
|
||||
const { showBackButton } = this.props;
|
||||
|
||||
return (
|
||||
@@ -176,10 +184,10 @@ export class ConversationHeader extends React.Component<Props> {
|
||||
<button
|
||||
onClick={this.showMenuBound}
|
||||
className={classNames(
|
||||
'module-conversation-header__gear-icon',
|
||||
'module-conversation-header__more-button',
|
||||
showBackButton
|
||||
? null
|
||||
: 'module-conversation-header__gear-icon--show'
|
||||
: 'module-conversation-header__more-button--show'
|
||||
)}
|
||||
disabled={showBackButton}
|
||||
/>
|
||||
@@ -187,6 +195,23 @@ export class ConversationHeader extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
public renderSearchButton() {
|
||||
const { onSearchInConversation, showBackButton } = this.props;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onSearchInConversation}
|
||||
className={classNames(
|
||||
'module-conversation-header__search-button',
|
||||
showBackButton
|
||||
? null
|
||||
: 'module-conversation-header__search-button--show'
|
||||
)}
|
||||
disabled={showBackButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMenu(triggerId: string) {
|
||||
const {
|
||||
i18n,
|
||||
@@ -260,7 +285,8 @@ export class ConversationHeader extends React.Component<Props> {
|
||||
</div>
|
||||
</div>
|
||||
{this.renderExpirationLength()}
|
||||
{this.renderGear(triggerId)}
|
||||
{this.renderSearchButton()}
|
||||
{this.renderMoreButton(triggerId)}
|
||||
{this.renderMenu(triggerId)}
|
||||
</div>
|
||||
);
|
||||
|
Reference in New Issue
Block a user