2019-01-14 21:49:58 +00:00
|
|
|
import { omit, reject } from 'lodash';
|
|
|
|
|
|
|
|
import { normalize } from '../../types/PhoneNumber';
|
|
|
|
import { trigger } from '../../shims/events';
|
2019-05-31 22:42:01 +00:00
|
|
|
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
2019-08-09 23:12:29 +00:00
|
|
|
import {
|
2019-09-04 14:46:28 +00:00
|
|
|
searchConversations as dataSearchConversations,
|
|
|
|
searchMessages as dataSearchMessages,
|
2019-08-09 23:12:29 +00:00
|
|
|
searchMessagesInConversation,
|
|
|
|
} from '../../../js/modules/data';
|
2019-01-14 21:49:58 +00:00
|
|
|
import { makeLookup } from '../../util/makeLookup';
|
|
|
|
|
|
|
|
import {
|
|
|
|
ConversationType,
|
2019-05-31 22:42:01 +00:00
|
|
|
MessageDeletedActionType,
|
2019-08-09 00:46:49 +00:00
|
|
|
MessageType,
|
2019-01-14 21:49:58 +00:00
|
|
|
RemoveAllConversationsActionType,
|
|
|
|
SelectedConversationChangedActionType,
|
|
|
|
} from './conversations';
|
|
|
|
|
|
|
|
// State
|
|
|
|
|
2019-08-09 00:46:49 +00:00
|
|
|
export type MessageSearchResultType = MessageType & {
|
|
|
|
snippet: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type MessageSearchResultLookupType = {
|
|
|
|
[id: string]: MessageSearchResultType;
|
|
|
|
};
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
export type SearchStateType = {
|
2019-08-09 23:12:29 +00:00
|
|
|
searchConversationId?: string;
|
|
|
|
searchConversationName?: string;
|
2019-08-09 00:46:49 +00:00
|
|
|
// We store just ids of conversations, since that data is always cached in memory
|
|
|
|
contacts: Array<string>;
|
|
|
|
conversations: Array<string>;
|
2019-01-14 21:49:58 +00:00
|
|
|
query: string;
|
|
|
|
normalizedPhoneNumber?: string;
|
2019-08-09 00:46:49 +00:00
|
|
|
messageIds: Array<string>;
|
|
|
|
// We do store message data to pass through the selector
|
|
|
|
messageLookup: MessageSearchResultLookupType;
|
2019-01-14 21:49:58 +00:00
|
|
|
selectedMessage?: string;
|
2019-09-04 14:46:28 +00:00
|
|
|
// Loading state
|
|
|
|
discussionsLoading: boolean;
|
|
|
|
messagesLoading: boolean;
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Actions
|
|
|
|
|
2019-09-04 14:46:28 +00:00
|
|
|
type SearchResultsBaseType = {
|
2019-01-14 21:49:58 +00:00
|
|
|
query: string;
|
|
|
|
normalizedPhoneNumber?: string;
|
2019-09-04 14:46:28 +00:00
|
|
|
};
|
|
|
|
type SearchMessagesResultsPayloadType = SearchResultsBaseType & {
|
2019-03-20 17:42:28 +00:00
|
|
|
messages: Array<MessageSearchResultType>;
|
2019-09-04 14:46:28 +00:00
|
|
|
};
|
|
|
|
type SearchDiscussionsResultsPayloadType = SearchResultsBaseType & {
|
2019-01-14 21:49:58 +00:00
|
|
|
conversations: Array<string>;
|
|
|
|
contacts: Array<string>;
|
|
|
|
};
|
2019-09-04 14:46:28 +00:00
|
|
|
type SearchMessagesResultsKickoffActionType = {
|
|
|
|
type: 'SEARCH_MESSAGES_RESULTS';
|
|
|
|
payload: Promise<SearchMessagesResultsPayloadType>;
|
|
|
|
};
|
|
|
|
type SearchDiscussionsResultsKickoffActionType = {
|
|
|
|
type: 'SEARCH_DISCUSSIONS_RESULTS';
|
|
|
|
payload: Promise<SearchDiscussionsResultsPayloadType>;
|
|
|
|
};
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2019-09-04 14:46:28 +00:00
|
|
|
type SearchMessagesResultsPendingActionType = {
|
|
|
|
type: 'SEARCH_MESSAGES_RESULTS_PENDING';
|
|
|
|
payload: SearchMessagesResultsPayloadType;
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
2019-09-04 14:46:28 +00:00
|
|
|
type SearchDiscussionsResultsPendingActionType = {
|
|
|
|
type: 'SEARCH_DISCUSSIONS_RESULTS_PENDING';
|
|
|
|
payload: SearchDiscussionsResultsPayloadType;
|
|
|
|
};
|
|
|
|
type SearchMessagesResultsFulfilledActionType = {
|
|
|
|
type: 'SEARCH_MESSAGES_RESULTS_FULFILLED';
|
|
|
|
payload: SearchMessagesResultsPayloadType;
|
|
|
|
};
|
|
|
|
type SearchDiscussionsResultsFulfilledActionType = {
|
|
|
|
type: 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED';
|
|
|
|
payload: SearchDiscussionsResultsPayloadType;
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
type UpdateSearchTermActionType = {
|
|
|
|
type: 'SEARCH_UPDATE';
|
|
|
|
payload: {
|
|
|
|
query: string;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
type ClearSearchActionType = {
|
|
|
|
type: 'SEARCH_CLEAR';
|
|
|
|
payload: null;
|
|
|
|
};
|
2019-08-09 23:12:29 +00:00
|
|
|
type ClearConversationSearchActionType = {
|
|
|
|
type: 'CLEAR_CONVERSATION_SEARCH';
|
|
|
|
payload: null;
|
|
|
|
};
|
|
|
|
type SearchInConversationActionType = {
|
|
|
|
type: 'SEARCH_IN_CONVERSATION';
|
|
|
|
payload: {
|
|
|
|
searchConversationId: string;
|
|
|
|
searchConversationName: string;
|
|
|
|
};
|
|
|
|
};
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
|
|
export type SEARCH_TYPES =
|
2019-09-04 14:46:28 +00:00
|
|
|
| SearchMessagesResultsKickoffActionType
|
|
|
|
| SearchDiscussionsResultsKickoffActionType
|
|
|
|
| SearchMessagesResultsPendingActionType
|
|
|
|
| SearchDiscussionsResultsPendingActionType
|
|
|
|
| SearchMessagesResultsFulfilledActionType
|
|
|
|
| SearchDiscussionsResultsFulfilledActionType
|
2019-01-14 21:49:58 +00:00
|
|
|
| UpdateSearchTermActionType
|
|
|
|
| ClearSearchActionType
|
2019-08-09 23:12:29 +00:00
|
|
|
| ClearConversationSearchActionType
|
|
|
|
| SearchInConversationActionType
|
2019-05-31 22:42:01 +00:00
|
|
|
| MessageDeletedActionType
|
2019-01-14 21:49:58 +00:00
|
|
|
| RemoveAllConversationsActionType
|
|
|
|
| SelectedConversationChangedActionType;
|
|
|
|
|
|
|
|
// Action Creators
|
|
|
|
|
|
|
|
export const actions = {
|
2019-09-04 14:46:28 +00:00
|
|
|
searchMessages,
|
|
|
|
searchDiscussions,
|
2019-01-14 21:49:58 +00:00
|
|
|
clearSearch,
|
2019-08-09 23:12:29 +00:00
|
|
|
clearConversationSearch,
|
|
|
|
searchInConversation,
|
2019-01-14 21:49:58 +00:00
|
|
|
updateSearchTerm,
|
|
|
|
startNewConversation,
|
|
|
|
};
|
|
|
|
|
2019-09-04 14:46:28 +00:00
|
|
|
function searchMessages(
|
2019-01-14 21:49:58 +00:00
|
|
|
query: string,
|
2019-08-09 23:12:29 +00:00
|
|
|
options: {
|
|
|
|
regionCode: string;
|
2019-09-04 14:46:28 +00:00
|
|
|
}
|
|
|
|
): SearchMessagesResultsKickoffActionType {
|
|
|
|
return {
|
|
|
|
type: 'SEARCH_MESSAGES_RESULTS',
|
|
|
|
payload: doSearchMessages(query, options),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function searchDiscussions(
|
|
|
|
query: string,
|
|
|
|
options: {
|
2019-08-09 23:12:29 +00:00
|
|
|
ourNumber: string;
|
|
|
|
noteToSelf: string;
|
|
|
|
}
|
2019-09-04 14:46:28 +00:00
|
|
|
): SearchDiscussionsResultsKickoffActionType {
|
2019-01-14 21:49:58 +00:00
|
|
|
return {
|
2019-09-04 14:46:28 +00:00
|
|
|
type: 'SEARCH_DISCUSSIONS_RESULTS',
|
|
|
|
payload: doSearchDiscussions(query, options),
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-09-04 14:46:28 +00:00
|
|
|
async function doSearchMessages(
|
2019-01-14 21:49:58 +00:00
|
|
|
query: string,
|
|
|
|
options: {
|
2019-08-09 23:12:29 +00:00
|
|
|
searchConversationId?: string;
|
2019-01-14 21:49:58 +00:00
|
|
|
regionCode: string;
|
|
|
|
}
|
2019-09-04 14:46:28 +00:00
|
|
|
): Promise<SearchMessagesResultsPayloadType> {
|
|
|
|
const { regionCode, searchConversationId } = options;
|
2019-08-09 23:12:29 +00:00
|
|
|
const normalizedPhoneNumber = normalize(query, { regionCode });
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2019-09-04 14:46:28 +00:00
|
|
|
const messages = await queryMessages(query, searchConversationId);
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2019-09-04 14:46:28 +00:00
|
|
|
return {
|
|
|
|
messages,
|
|
|
|
normalizedPhoneNumber,
|
|
|
|
query,
|
|
|
|
};
|
|
|
|
}
|
2019-08-09 23:12:29 +00:00
|
|
|
|
2019-09-04 14:46:28 +00:00
|
|
|
async function doSearchDiscussions(
|
|
|
|
query: string,
|
|
|
|
options: {
|
|
|
|
ourNumber: string;
|
|
|
|
noteToSelf: string;
|
2019-08-09 23:12:29 +00:00
|
|
|
}
|
2019-09-04 14:46:28 +00:00
|
|
|
): Promise<SearchDiscussionsResultsPayloadType> {
|
|
|
|
const { ourNumber, noteToSelf } = options;
|
|
|
|
const { conversations, contacts } = await queryConversationsAndContacts(
|
|
|
|
query,
|
|
|
|
{
|
|
|
|
ourNumber,
|
|
|
|
noteToSelf,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
conversations,
|
|
|
|
contacts,
|
|
|
|
query,
|
|
|
|
};
|
2019-01-14 21:49:58 +00:00
|
|
|
}
|
2019-09-04 14:46:28 +00:00
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
function clearSearch(): ClearSearchActionType {
|
|
|
|
return {
|
|
|
|
type: 'SEARCH_CLEAR',
|
|
|
|
payload: null,
|
|
|
|
};
|
|
|
|
}
|
2019-08-09 23:12:29 +00:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
function updateSearchTerm(query: string): UpdateSearchTermActionType {
|
|
|
|
return {
|
|
|
|
type: 'SEARCH_UPDATE',
|
|
|
|
payload: {
|
|
|
|
query,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
function startNewConversation(
|
|
|
|
query: string,
|
|
|
|
options: { regionCode: string }
|
|
|
|
): ClearSearchActionType {
|
|
|
|
const { regionCode } = options;
|
|
|
|
const normalized = normalize(query, { regionCode });
|
|
|
|
if (!normalized) {
|
|
|
|
throw new Error('Attempted to start new conversation with invalid number');
|
|
|
|
}
|
|
|
|
trigger('showConversation', normalized);
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: 'SEARCH_CLEAR',
|
|
|
|
payload: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-08-09 23:12:29 +00:00
|
|
|
async function queryMessages(query: string, searchConversationId?: string) {
|
2019-05-31 22:42:01 +00:00
|
|
|
try {
|
|
|
|
const normalized = cleanSearchTerm(query);
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2019-08-09 23:12:29 +00:00
|
|
|
if (searchConversationId) {
|
|
|
|
return searchMessagesInConversation(normalized, searchConversationId);
|
|
|
|
}
|
|
|
|
|
2019-09-04 14:46:28 +00:00
|
|
|
return dataSearchMessages(normalized);
|
2019-05-31 22:42:01 +00:00
|
|
|
} catch (e) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
|
|
async function queryConversationsAndContacts(
|
|
|
|
providedQuery: string,
|
|
|
|
options: { ourNumber: string; noteToSelf: string }
|
|
|
|
) {
|
|
|
|
const { ourNumber, noteToSelf } = options;
|
|
|
|
const query = providedQuery.replace(/[+-.()]*/g, '');
|
|
|
|
|
2019-09-04 14:46:28 +00:00
|
|
|
const searchResults: Array<ConversationType> = await dataSearchConversations(
|
2019-01-14 21:49:58 +00:00
|
|
|
query
|
|
|
|
);
|
|
|
|
|
|
|
|
// Split into two groups - active conversations and items just from address book
|
|
|
|
let conversations: Array<string> = [];
|
|
|
|
let contacts: Array<string> = [];
|
|
|
|
const max = searchResults.length;
|
|
|
|
for (let i = 0; i < max; i += 1) {
|
|
|
|
const conversation = searchResults[i];
|
|
|
|
|
|
|
|
if (conversation.type === 'direct' && !Boolean(conversation.lastMessage)) {
|
|
|
|
contacts.push(conversation.id);
|
|
|
|
} else {
|
|
|
|
conversations.push(conversation.id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Inject synthetic Note to Self entry if query matches localized 'Note to Self'
|
|
|
|
if (noteToSelf.indexOf(providedQuery.toLowerCase()) !== -1) {
|
|
|
|
// ensure that we don't have duplicates in our results
|
|
|
|
contacts = contacts.filter(id => id !== ourNumber);
|
|
|
|
conversations = conversations.filter(id => id !== ourNumber);
|
|
|
|
|
|
|
|
contacts.unshift(ourNumber);
|
|
|
|
}
|
|
|
|
|
|
|
|
return { conversations, contacts };
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reducer
|
|
|
|
|
|
|
|
function getEmptyState(): SearchStateType {
|
|
|
|
return {
|
|
|
|
query: '',
|
2019-08-09 00:46:49 +00:00
|
|
|
messageIds: [],
|
2019-01-14 21:49:58 +00:00
|
|
|
messageLookup: {},
|
|
|
|
conversations: [],
|
|
|
|
contacts: [],
|
2019-09-04 14:46:28 +00:00
|
|
|
discussionsLoading: false,
|
|
|
|
messagesLoading: false,
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-08-09 23:12:29 +00:00
|
|
|
// tslint:disable-next-line max-func-body-length
|
2019-01-14 21:49:58 +00:00
|
|
|
export function reducer(
|
2019-05-16 22:32:11 +00:00
|
|
|
state: SearchStateType = getEmptyState(),
|
2019-01-14 21:49:58 +00:00
|
|
|
action: SEARCH_TYPES
|
|
|
|
): SearchStateType {
|
|
|
|
if (action.type === 'SEARCH_CLEAR') {
|
|
|
|
return getEmptyState();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === 'SEARCH_UPDATE') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { query } = payload;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
query,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-08-09 23:12:29 +00:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-09-04 14:46:28 +00:00
|
|
|
if (action.type === 'SEARCH_MESSAGES_RESULTS_PENDING') {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messageIds: [],
|
|
|
|
messageLookup: {},
|
|
|
|
messagesLoading: true,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === 'SEARCH_DISCUSSIONS_RESULTS_PENDING') {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
contacts: [],
|
|
|
|
conversations: [],
|
|
|
|
discussionsLoading: true,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === 'SEARCH_MESSAGES_RESULTS_FULFILLED') {
|
2019-01-14 21:49:58 +00:00
|
|
|
const { payload } = action;
|
2019-09-04 14:46:28 +00:00
|
|
|
const { messages, normalizedPhoneNumber, query } = payload;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
|
|
// Reject if the associated query is not the most recent user-provided query
|
|
|
|
if (state.query !== query) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2019-08-09 00:46:49 +00:00
|
|
|
const messageIds = messages.map(message => message.id);
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
return {
|
|
|
|
...state,
|
2019-08-09 00:46:49 +00:00
|
|
|
normalizedPhoneNumber,
|
|
|
|
query,
|
|
|
|
messageIds,
|
2019-01-14 21:49:58 +00:00
|
|
|
messageLookup: makeLookup(messages, 'id'),
|
2019-09-04 14:46:28 +00:00
|
|
|
messagesLoading: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { contacts, conversations } = payload;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
contacts,
|
|
|
|
conversations,
|
|
|
|
discussionsLoading: false,
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
|
|
|
|
return getEmptyState();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
|
|
|
|
const { payload } = action;
|
2019-08-09 23:12:29 +00:00
|
|
|
const { id, messageId } = payload;
|
|
|
|
const { searchConversationId } = state;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2019-08-09 23:12:29 +00:00
|
|
|
if (searchConversationId && searchConversationId !== id) {
|
|
|
|
return getEmptyState();
|
2019-01-14 21:49:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
selectedMessage: messageId,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
if (action.type === 'MESSAGE_DELETED') {
|
2019-08-09 00:46:49 +00:00
|
|
|
const { messageIds, messageLookup } = state;
|
|
|
|
if (!messageIds || messageIds.length < 1) {
|
2019-01-14 21:49:58 +00:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { payload } = action;
|
|
|
|
const { id } = payload;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
2019-08-09 00:46:49 +00:00
|
|
|
messageIds: reject(messageIds, messageId => id === messageId),
|
2019-01-14 21:49:58 +00:00
|
|
|
messageLookup: omit(messageLookup, ['id']),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return state;
|
|
|
|
}
|