2020-10-30 20:34:04 +00:00
|
|
|
// Copyright 2019-2020 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2019-08-09 00:46:49 +00:00
|
|
|
import memoizee from 'memoizee';
|
2019-01-14 21:49:58 +00:00
|
|
|
import { createSelector } from 'reselect';
|
2020-03-10 00:43:09 +00:00
|
|
|
import { instance } from '../../util/libphonenumberInstance';
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
|
|
import { StateType } from '../reducer';
|
|
|
|
|
|
|
|
import {
|
2019-08-09 00:46:49 +00:00
|
|
|
MessageSearchResultLookupType,
|
|
|
|
MessageSearchResultType,
|
|
|
|
SearchStateType,
|
|
|
|
} from '../ducks/search';
|
|
|
|
import {
|
|
|
|
ConversationLookupType,
|
|
|
|
ConversationType,
|
|
|
|
} from '../ducks/conversations';
|
|
|
|
|
|
|
|
import {
|
|
|
|
PropsDataType as SearchResultsPropsType,
|
|
|
|
SearchResultRowType,
|
|
|
|
} from '../../components/SearchResults';
|
|
|
|
import { PropsDataType as MessageSearchResultPropsDataType } from '../../components/MessageSearchResult';
|
|
|
|
|
2021-01-06 15:41:43 +00:00
|
|
|
import { getRegionCode, getUserConversationId } from './user';
|
2020-12-04 20:41:40 +00:00
|
|
|
import { getUserAgent } from './items';
|
2019-08-09 00:46:49 +00:00
|
|
|
import {
|
|
|
|
GetConversationByIdType,
|
2019-01-14 21:49:58 +00:00
|
|
|
getConversationLookup,
|
2019-08-09 00:46:49 +00:00
|
|
|
getConversationSelector,
|
2019-01-14 21:49:58 +00:00
|
|
|
getSelectedConversation,
|
|
|
|
} from './conversations';
|
2019-03-12 00:20:16 +00:00
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
export const getSearch = (state: StateType): SearchStateType => state.search;
|
|
|
|
|
|
|
|
export const getQuery = createSelector(
|
|
|
|
getSearch,
|
|
|
|
(state: SearchStateType): string => state.query
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getSelectedMessage = createSelector(
|
|
|
|
getSearch,
|
|
|
|
(state: SearchStateType): string | undefined => state.selectedMessage
|
|
|
|
);
|
|
|
|
|
2019-08-09 23:12:29 +00:00
|
|
|
export const getSearchConversationId = createSelector(
|
|
|
|
getSearch,
|
|
|
|
(state: SearchStateType): string | undefined => state.searchConversationId
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getSearchConversationName = createSelector(
|
|
|
|
getSearch,
|
|
|
|
(state: SearchStateType): string | undefined => state.searchConversationName
|
|
|
|
);
|
|
|
|
|
2019-11-07 21:36:16 +00:00
|
|
|
export const getStartSearchCounter = createSelector(
|
|
|
|
getSearch,
|
|
|
|
(state: SearchStateType): number => state.startSearchCounter
|
|
|
|
);
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
export const isSearching = createSelector(
|
|
|
|
getSearch,
|
|
|
|
(state: SearchStateType) => {
|
2019-08-20 19:34:52 +00:00
|
|
|
const { query } = state;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2019-08-20 19:34:52 +00:00
|
|
|
return query && query.trim().length > 1;
|
2019-01-14 21:49:58 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2019-08-09 00:46:49 +00:00
|
|
|
export const getMessageSearchResultLookup = createSelector(
|
|
|
|
getSearch,
|
|
|
|
(state: SearchStateType) => state.messageLookup
|
|
|
|
);
|
2019-01-14 21:49:58 +00:00
|
|
|
export const getSearchResults = createSelector(
|
2019-11-07 21:36:16 +00:00
|
|
|
[
|
|
|
|
getSearch,
|
|
|
|
getRegionCode,
|
2020-03-10 00:43:09 +00:00
|
|
|
getUserAgent,
|
2019-11-07 21:36:16 +00:00
|
|
|
getConversationLookup,
|
|
|
|
getSelectedConversation,
|
|
|
|
getSelectedMessage,
|
|
|
|
],
|
2019-01-14 21:49:58 +00:00
|
|
|
(
|
|
|
|
state: SearchStateType,
|
2019-03-12 00:20:16 +00:00
|
|
|
regionCode: string,
|
2020-03-10 00:43:09 +00:00
|
|
|
userAgent: string,
|
2019-01-14 21:49:58 +00:00
|
|
|
lookup: ConversationLookupType,
|
2019-11-07 21:36:16 +00:00
|
|
|
selectedConversationId?: string,
|
|
|
|
selectedMessageId?: string
|
2019-08-20 19:34:52 +00:00
|
|
|
): SearchResultsPropsType | undefined => {
|
2019-08-09 23:12:29 +00:00
|
|
|
const {
|
|
|
|
contacts,
|
|
|
|
conversations,
|
2019-09-04 14:46:28 +00:00
|
|
|
discussionsLoading,
|
2019-08-09 23:12:29 +00:00
|
|
|
messageIds,
|
2019-09-04 14:46:28 +00:00
|
|
|
messagesLoading,
|
2019-08-09 23:12:29 +00:00
|
|
|
searchConversationName,
|
|
|
|
} = state;
|
2019-08-09 00:46:49 +00:00
|
|
|
|
|
|
|
const showStartNewConversation = Boolean(
|
|
|
|
state.normalizedPhoneNumber && !lookup[state.normalizedPhoneNumber]
|
|
|
|
);
|
|
|
|
const haveConversations = conversations && conversations.length;
|
|
|
|
const haveContacts = contacts && contacts.length;
|
|
|
|
const haveMessages = messageIds && messageIds.length;
|
|
|
|
const noResults =
|
2019-09-04 14:46:28 +00:00
|
|
|
!discussionsLoading &&
|
|
|
|
!messagesLoading &&
|
2019-08-09 00:46:49 +00:00
|
|
|
!showStartNewConversation &&
|
|
|
|
!haveConversations &&
|
|
|
|
!haveContacts &&
|
|
|
|
!haveMessages;
|
|
|
|
|
|
|
|
const items: Array<SearchResultRowType> = [];
|
|
|
|
|
|
|
|
if (showStartNewConversation) {
|
|
|
|
items.push({
|
|
|
|
type: 'start-new-conversation',
|
|
|
|
data: undefined,
|
|
|
|
});
|
2020-03-10 00:43:09 +00:00
|
|
|
|
|
|
|
const isIOS = userAgent === 'OWI';
|
2020-04-23 19:26:20 +00:00
|
|
|
let isValidNumber = false;
|
|
|
|
try {
|
|
|
|
// Sometimes parse() throws, like for invalid country codes
|
|
|
|
const parsedNumber = instance.parse(state.query, regionCode);
|
|
|
|
isValidNumber = instance.isValidNumber(parsedNumber);
|
|
|
|
} catch (_) {
|
|
|
|
// no-op
|
|
|
|
}
|
2020-03-10 00:43:09 +00:00
|
|
|
|
|
|
|
if (!isIOS && isValidNumber) {
|
|
|
|
items.push({
|
|
|
|
type: 'sms-mms-not-supported-text',
|
|
|
|
data: undefined,
|
|
|
|
});
|
|
|
|
}
|
2019-08-09 00:46:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (haveConversations) {
|
|
|
|
items.push({
|
|
|
|
type: 'conversations-header',
|
|
|
|
data: undefined,
|
|
|
|
});
|
|
|
|
conversations.forEach(id => {
|
|
|
|
const data = lookup[id];
|
|
|
|
items.push({
|
|
|
|
type: 'conversation',
|
|
|
|
data: {
|
|
|
|
...data,
|
2019-11-07 21:36:16 +00:00
|
|
|
isSelected: Boolean(data && id === selectedConversationId),
|
2019-08-09 00:46:49 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
2019-09-04 14:46:28 +00:00
|
|
|
} else if (discussionsLoading) {
|
|
|
|
items.push({
|
|
|
|
type: 'conversations-header',
|
|
|
|
data: undefined,
|
|
|
|
});
|
|
|
|
items.push({
|
|
|
|
type: 'spinner',
|
|
|
|
data: undefined,
|
|
|
|
});
|
2019-08-09 00:46:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (haveContacts) {
|
|
|
|
items.push({
|
|
|
|
type: 'contacts-header',
|
|
|
|
data: undefined,
|
|
|
|
});
|
|
|
|
contacts.forEach(id => {
|
|
|
|
const data = lookup[id];
|
2020-03-05 21:14:58 +00:00
|
|
|
|
2019-08-09 00:46:49 +00:00
|
|
|
items.push({
|
|
|
|
type: 'contact',
|
|
|
|
data: {
|
|
|
|
...data,
|
2019-11-07 21:36:16 +00:00
|
|
|
isSelected: Boolean(data && id === selectedConversationId),
|
2019-08-09 00:46:49 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (haveMessages) {
|
|
|
|
items.push({
|
|
|
|
type: 'messages-header',
|
|
|
|
data: undefined,
|
|
|
|
});
|
|
|
|
messageIds.forEach(messageId => {
|
|
|
|
items.push({
|
|
|
|
type: 'message',
|
|
|
|
data: messageId,
|
|
|
|
});
|
|
|
|
});
|
2019-09-04 14:46:28 +00:00
|
|
|
} else if (messagesLoading) {
|
|
|
|
items.push({
|
|
|
|
type: 'messages-header',
|
|
|
|
data: undefined,
|
|
|
|
});
|
|
|
|
items.push({
|
|
|
|
type: 'spinner',
|
|
|
|
data: undefined,
|
|
|
|
});
|
2019-08-09 00:46:49 +00:00
|
|
|
}
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
return {
|
2019-09-04 14:46:28 +00:00
|
|
|
discussionsLoading,
|
2019-08-09 00:46:49 +00:00
|
|
|
items,
|
2019-09-04 14:46:28 +00:00
|
|
|
messagesLoading,
|
2019-08-09 00:46:49 +00:00
|
|
|
noResults,
|
2020-09-14 21:56:35 +00:00
|
|
|
regionCode,
|
2019-08-09 23:12:29 +00:00
|
|
|
searchConversationName,
|
2019-01-14 21:49:58 +00:00
|
|
|
searchTerm: state.query,
|
2019-11-07 21:36:16 +00:00
|
|
|
selectedConversationId,
|
|
|
|
selectedMessageId,
|
2019-08-09 00:46:49 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export function _messageSearchResultSelector(
|
|
|
|
message: MessageSearchResultType,
|
2021-01-06 15:41:43 +00:00
|
|
|
from: ConversationType,
|
|
|
|
to: ConversationType,
|
2019-08-09 23:12:29 +00:00
|
|
|
searchConversationId?: string,
|
2019-08-09 00:46:49 +00:00
|
|
|
selectedMessageId?: string
|
|
|
|
): MessageSearchResultPropsDataType {
|
|
|
|
return {
|
2021-01-06 15:41:43 +00:00
|
|
|
from,
|
|
|
|
to,
|
|
|
|
|
|
|
|
id: message.id,
|
|
|
|
conversationId: message.conversationId,
|
|
|
|
sentAt: message.sent_at,
|
|
|
|
snippet: message.snippet,
|
|
|
|
|
|
|
|
isSelected: Boolean(selectedMessageId && message.id === selectedMessageId),
|
2019-08-09 23:12:29 +00:00
|
|
|
isSearchingInConversation: Boolean(searchConversationId),
|
2019-08-09 00:46:49 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// A little optimization to reset our selector cache whenever high-level application data
|
|
|
|
// changes: regionCode and userNumber.
|
|
|
|
type CachedMessageSearchResultSelectorType = (
|
|
|
|
message: MessageSearchResultType,
|
2021-01-06 15:41:43 +00:00
|
|
|
from: ConversationType,
|
|
|
|
to: ConversationType,
|
2019-08-09 23:12:29 +00:00
|
|
|
searchConversationId?: string,
|
2019-08-09 00:46:49 +00:00
|
|
|
selectedMessageId?: string
|
|
|
|
) => MessageSearchResultPropsDataType;
|
|
|
|
export const getCachedSelectorForMessageSearchResult = createSelector(
|
2021-01-06 15:41:43 +00:00
|
|
|
getUserConversationId,
|
2019-08-09 00:46:49 +00:00
|
|
|
(): CachedMessageSearchResultSelectorType => {
|
|
|
|
// Note: memoizee will check all parameters provided, and only run our selector
|
|
|
|
// if any of them have changed.
|
|
|
|
return memoizee(_messageSearchResultSelector, { max: 500 });
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
type GetMessageSearchResultByIdType = (
|
|
|
|
id: string
|
|
|
|
) => MessageSearchResultPropsDataType | undefined;
|
|
|
|
export const getMessageSearchResultSelector = createSelector(
|
|
|
|
getCachedSelectorForMessageSearchResult,
|
|
|
|
getMessageSearchResultLookup,
|
|
|
|
getSelectedMessage,
|
|
|
|
getConversationSelector,
|
2019-08-09 23:12:29 +00:00
|
|
|
getSearchConversationId,
|
2021-01-06 15:41:43 +00:00
|
|
|
getUserConversationId,
|
2019-08-09 00:46:49 +00:00
|
|
|
(
|
|
|
|
messageSearchResultSelector: CachedMessageSearchResultSelectorType,
|
|
|
|
messageSearchResultLookup: MessageSearchResultLookupType,
|
2021-01-06 15:41:43 +00:00
|
|
|
selectedMessageId: string | undefined,
|
2019-08-09 00:46:49 +00:00
|
|
|
conversationSelector: GetConversationByIdType,
|
2019-08-09 23:12:29 +00:00
|
|
|
searchConversationId: string | undefined,
|
2021-01-06 15:41:43 +00:00
|
|
|
ourConversationId: string
|
2019-08-09 00:46:49 +00:00
|
|
|
): GetMessageSearchResultByIdType => {
|
|
|
|
return (id: string) => {
|
|
|
|
const message = messageSearchResultLookup[id];
|
|
|
|
if (!message) {
|
2021-01-06 15:41:43 +00:00
|
|
|
window.log.warn(
|
|
|
|
`getMessageSearchResultSelector: messageSearchResultLookup was missing id ${id}`
|
|
|
|
);
|
2020-09-14 21:56:35 +00:00
|
|
|
return undefined;
|
2019-08-09 00:46:49 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 15:41:43 +00:00
|
|
|
const { conversationId, source, sourceUuid, type } = message;
|
|
|
|
let from: ConversationType;
|
|
|
|
let to: ConversationType;
|
2019-08-09 00:46:49 +00:00
|
|
|
|
|
|
|
if (type === 'incoming') {
|
2021-01-06 15:41:43 +00:00
|
|
|
from = conversationSelector(sourceUuid || source);
|
|
|
|
to = conversationSelector(conversationId);
|
2019-08-09 00:46:49 +00:00
|
|
|
} else if (type === 'outgoing') {
|
2021-01-06 15:41:43 +00:00
|
|
|
from = conversationSelector(ourConversationId);
|
|
|
|
to = conversationSelector(conversationId);
|
|
|
|
} else {
|
|
|
|
window.log.warn(
|
|
|
|
`getMessageSearchResultSelector: Got unexpected type ${type}`
|
|
|
|
);
|
|
|
|
return undefined;
|
2019-08-09 00:46:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return messageSearchResultSelector(
|
|
|
|
message,
|
2021-01-06 15:41:43 +00:00
|
|
|
from,
|
|
|
|
to,
|
2019-08-09 23:12:29 +00:00
|
|
|
searchConversationId,
|
2021-01-06 15:41:43 +00:00
|
|
|
selectedMessageId
|
2019-08-09 00:46:49 +00:00
|
|
|
);
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|