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

@ -3,7 +3,11 @@ import { omit, reject } from 'lodash';
import { normalize } from '../../types/PhoneNumber';
import { trigger } from '../../shims/events';
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import { searchConversations, searchMessages } from '../../../js/modules/data';
import {
searchConversations,
searchMessages,
searchMessagesInConversation,
} from '../../../js/modules/data';
import { makeLookup } from '../../util/makeLookup';
import {
@ -25,6 +29,8 @@ export type MessageSearchResultLookupType = {
};
export type SearchStateType = {
searchConversationId?: string;
searchConversationName?: string;
// We store just ids of conversations, since that data is always cached in memory
contacts: Array<string>;
conversations: Array<string>;
@ -64,11 +70,24 @@ type ClearSearchActionType = {
type: 'SEARCH_CLEAR';
payload: null;
};
type ClearConversationSearchActionType = {
type: 'CLEAR_CONVERSATION_SEARCH';
payload: null;
};
type SearchInConversationActionType = {
type: 'SEARCH_IN_CONVERSATION';
payload: {
searchConversationId: string;
searchConversationName: string;
};
};
export type SEARCH_TYPES =
| SearchResultsFulfilledActionType
| UpdateSearchTermActionType
| ClearSearchActionType
| ClearConversationSearchActionType
| SearchInConversationActionType
| MessageDeletedActionType
| RemoveAllConversationsActionType
| SelectedConversationChangedActionType;
@ -78,13 +97,20 @@ export type SEARCH_TYPES =
export const actions = {
search,
clearSearch,
clearConversationSearch,
searchInConversation,
updateSearchTerm,
startNewConversation,
};
function search(
query: string,
options: { regionCode: string; ourNumber: string; noteToSelf: string }
options: {
searchConversationId?: string;
regionCode: string;
ourNumber: string;
noteToSelf: string;
}
): SearchResultsKickoffActionType {
return {
type: 'SEARCH_RESULTS',
@ -95,26 +121,40 @@ function search(
async function doSearch(
query: string,
options: {
searchConversationId?: string;
regionCode: string;
ourNumber: string;
noteToSelf: string;
}
): Promise<SearchResultsPayloadType> {
const { regionCode, ourNumber, noteToSelf } = options;
const { regionCode, ourNumber, noteToSelf, searchConversationId } = options;
const normalizedPhoneNumber = normalize(query, { regionCode });
const [discussions, messages] = await Promise.all([
queryConversationsAndContacts(query, { ourNumber, noteToSelf }),
queryMessages(query),
]);
const { conversations, contacts } = discussions;
if (searchConversationId) {
const messages = await queryMessages(query, searchConversationId);
return {
query,
normalizedPhoneNumber: normalize(query, { regionCode }),
conversations,
contacts,
messages,
};
return {
contacts: [],
conversations: [],
messages,
normalizedPhoneNumber,
query,
};
} else {
const [discussions, messages] = await Promise.all([
queryConversationsAndContacts(query, { ourNumber, noteToSelf }),
queryMessages(query),
]);
const { conversations, contacts } = discussions;
return {
contacts,
conversations,
messages,
normalizedPhoneNumber,
query,
};
}
}
function clearSearch(): ClearSearchActionType {
return {
@ -122,6 +162,25 @@ function clearSearch(): ClearSearchActionType {
payload: null,
};
}
function clearConversationSearch(): ClearConversationSearchActionType {
return {
type: 'CLEAR_CONVERSATION_SEARCH',
payload: null,
};
}
function searchInConversation(
searchConversationId: string,
searchConversationName: string
): SearchInConversationActionType {
return {
type: 'SEARCH_IN_CONVERSATION',
payload: {
searchConversationId,
searchConversationName,
},
};
}
function updateSearchTerm(query: string): UpdateSearchTermActionType {
return {
type: 'SEARCH_UPDATE',
@ -147,10 +206,14 @@ function startNewConversation(
};
}
async function queryMessages(query: string) {
async function queryMessages(query: string, searchConversationId?: string) {
try {
const normalized = cleanSearchTerm(query);
if (searchConversationId) {
return searchMessagesInConversation(normalized, searchConversationId);
}
return searchMessages(normalized);
} catch (e) {
return [];
@ -206,6 +269,7 @@ function getEmptyState(): SearchStateType {
};
}
// tslint:disable-next-line max-func-body-length
export function reducer(
state: SearchStateType = getEmptyState(),
action: SEARCH_TYPES
@ -224,6 +288,30 @@ export function reducer(
};
}
if (action.type === 'SEARCH_IN_CONVERSATION') {
const { payload } = action;
const { searchConversationId, searchConversationName } = payload;
if (searchConversationId === state.searchConversationId) {
return state;
}
return {
...getEmptyState(),
searchConversationId,
searchConversationName,
};
}
if (action.type === 'CLEAR_CONVERSATION_SEARCH') {
const { searchConversationId, searchConversationName } = state;
return {
...getEmptyState(),
searchConversationId,
searchConversationName,
};
}
if (action.type === 'SEARCH_RESULTS_FULFILLED') {
const { payload } = action;
const {
@ -258,10 +346,11 @@ export function reducer(
if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
const { payload } = action;
const { messageId } = payload;
const { id, messageId } = payload;
const { searchConversationId } = state;
if (!messageId) {
return state;
if (searchConversationId && searchConversationId !== id) {
return getEmptyState();
}
return {

View file

@ -40,12 +40,22 @@ export const getSelectedMessage = createSelector(
(state: SearchStateType): string | undefined => state.selectedMessage
);
export const getSearchConversationId = createSelector(
getSearch,
(state: SearchStateType): string | undefined => state.searchConversationId
);
export const getSearchConversationName = createSelector(
getSearch,
(state: SearchStateType): string | undefined => state.searchConversationName
);
export const isSearching = createSelector(
getSearch,
(state: SearchStateType) => {
const { query } = state;
const { query, searchConversationId } = state;
return query && query.trim().length > 1;
return (query && query.trim().length > 1) || searchConversationId;
}
);
@ -62,7 +72,12 @@ export const getSearchResults = createSelector(
lookup: ConversationLookupType,
selectedConversation?: string
): SearchResultsPropsType => {
const { conversations, contacts, messageIds } = state;
const {
contacts,
conversations,
messageIds,
searchConversationName,
} = state;
const showStartNewConversation = Boolean(
state.normalizedPhoneNumber && !lookup[state.normalizedPhoneNumber]
@ -136,6 +151,7 @@ export const getSearchResults = createSelector(
items,
noResults,
regionCode: regionCode,
searchConversationName,
searchTerm: state.query,
};
}
@ -151,6 +167,7 @@ export function _messageSearchResultSelector(
sender?: ConversationType,
// @ts-ignore
recipient?: ConversationType,
searchConversationId?: string,
selectedMessageId?: string
): MessageSearchResultPropsDataType {
// Note: We don't use all of those parameters here, but the shim we call does.
@ -158,6 +175,7 @@ export function _messageSearchResultSelector(
return {
...getSearchResultsProps(message),
isSelected: message.id === selectedMessageId,
isSearchingInConversation: Boolean(searchConversationId),
};
}
@ -169,6 +187,7 @@ type CachedMessageSearchResultSelectorType = (
regionCode: string,
sender?: ConversationType,
recipient?: ConversationType,
searchConversationId?: string,
selectedMessageId?: string
) => MessageSearchResultPropsDataType;
export const getCachedSelectorForMessageSearchResult = createSelector(
@ -189,6 +208,7 @@ export const getMessageSearchResultSelector = createSelector(
getMessageSearchResultLookup,
getSelectedMessage,
getConversationSelector,
getSearchConversationId,
getRegionCode,
getUserNumber,
(
@ -196,6 +216,7 @@ export const getMessageSearchResultSelector = createSelector(
messageSearchResultLookup: MessageSearchResultLookupType,
selectedMessage: string | undefined,
conversationSelector: GetConversationByIdType,
searchConversationId: string | undefined,
regionCode: string,
ourNumber: string
): GetMessageSearchResultByIdType => {
@ -223,6 +244,7 @@ export const getMessageSearchResultSelector = createSelector(
regionCode,
sender,
recipient,
searchConversationId,
selectedMessage
);
};

View file

@ -4,13 +4,19 @@ import { mapDispatchToProps } from '../actions';
import { MainHeader } from '../../components/MainHeader';
import { StateType } from '../reducer';
import { getQuery } from '../selectors/search';
import {
getQuery,
getSearchConversationId,
getSearchConversationName,
} from '../selectors/search';
import { getIntl, getRegionCode, getUserNumber } from '../selectors/user';
import { getMe } from '../selectors/conversations';
const mapStateToProps = (state: StateType) => {
return {
searchTerm: getQuery(state),
searchConversationId: getSearchConversationId(state),
searchConversationName: getSearchConversationName(state),
regionCode: getRegionCode(state),
ourNumber: getUserNumber(state),
...getMe(state),