Full-text search within conversation
This commit is contained in:
parent
6292019d30
commit
c39d5a811a
26 changed files with 697 additions and 134 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue