Split search actions between discussions and messages

This commit is contained in:
Ken Powers 2019-09-04 10:46:28 -04:00 committed by GitHub
parent 1ab844674a
commit 1d3fe4bbf3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 213 additions and 93 deletions

View file

@ -3219,20 +3219,6 @@
padding-right: 1em; padding-right: 1em;
width: 100%; width: 100%;
text-align: center; text-align: center;
animation: delayed-fade-in 2s;
}
@keyframes delayed-fade-in {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
} }
.module-search-results__contacts-header { .module-search-results__contacts-header {
@ -3257,6 +3243,13 @@
letter-spacing: 0; letter-spacing: 0;
} }
.module-search-results__spinner-container {
width: 100%;
padding: 10px;
text-align: center;
}
// Module: Message Search Result // Module: Message Search Result
.module-message-search-result { .module-message-search-result {

View file

@ -1396,7 +1396,7 @@ body.dark-theme {
} }
&:focus { &:focus {
border: solid 1px blue; border: solid 1px $color-signal-blue;
outline: none; outline: none;
} }
} }

View file

@ -25,11 +25,16 @@ export interface PropsType {
i18n: LocalizerType; i18n: LocalizerType;
updateSearchTerm: (searchTerm: string) => void; updateSearchTerm: (searchTerm: string) => void;
search: ( searchMessages: (
query: string, query: string,
options: { options: {
searchConversationId?: string; searchConversationId?: string;
regionCode: string; regionCode: string;
}
) => void;
searchDiscussions: (
query: string,
options: {
ourNumber: string; ourNumber: string;
noteToSelf: string; noteToSelf: string;
} }
@ -66,15 +71,21 @@ export class MainHeader extends React.Component<PropsType> {
i18n, i18n,
ourNumber, ourNumber,
regionCode, regionCode,
search, searchDiscussions,
searchMessages,
searchConversationId, searchConversationId,
} = this.props; } = this.props;
if (search) { if (searchDiscussions) {
search(searchTerm, { searchDiscussions(searchTerm, {
searchConversationId,
noteToSelf: i18n('noteToSelf').toLowerCase(), noteToSelf: i18n('noteToSelf').toLowerCase(),
ourNumber, ourNumber,
});
}
if (searchMessages) {
searchMessages(searchTerm, {
searchConversationId,
regionCode, regionCode,
}); });
} }

View file

@ -8,6 +8,7 @@ import {
import { Intl } from './Intl'; import { Intl } from './Intl';
import { Emojify } from './conversation/Emojify'; import { Emojify } from './conversation/Emojify';
import { Spinner } from './Spinner';
import { import {
ConversationListItem, ConversationListItem,
PropsData as ConversationListItemPropsType, PropsData as ConversationListItemPropsType,
@ -17,11 +18,13 @@ import { StartNewConversation } from './StartNewConversation';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
export type PropsDataType = { export type PropsDataType = {
discussionsLoading: boolean;
items: Array<SearchResultRowType>; items: Array<SearchResultRowType>;
messagesLoading: boolean;
noResults: boolean; noResults: boolean;
regionCode: string; regionCode: string;
searchTerm: string;
searchConversationName?: string; searchConversationName?: string;
searchTerm: string;
}; };
type StartNewConversationType = { type StartNewConversationType = {
@ -52,6 +55,10 @@ type MessageType = {
type: 'message'; type: 'message';
data: string; data: string;
}; };
type SpinnerType = {
type: 'spinner';
data: undefined;
};
export type SearchResultRowType = export type SearchResultRowType =
| StartNewConversationType | StartNewConversationType
@ -60,7 +67,8 @@ export type SearchResultRowType =
| MessagesHeaderType | MessagesHeaderType
| ConversationType | ConversationType
| ContactsType | ContactsType
| MessageType; | MessageType
| SpinnerType;
type PropsHousekeepingType = { type PropsHousekeepingType = {
i18n: LocalizerType; i18n: LocalizerType;
@ -89,7 +97,7 @@ export class SearchResults extends React.Component<PropsType> {
public mostRecentWidth = 0; public mostRecentWidth = 0;
public mostRecentHeight = 0; public mostRecentHeight = 0;
public cellSizeCache = new CellMeasurerCache({ public cellSizeCache = new CellMeasurerCache({
defaultHeight: 36, defaultHeight: 80,
fixedWidth: true, fixedWidth: true,
}); });
public listRef = React.createRef<any>(); public listRef = React.createRef<any>();
@ -160,6 +168,12 @@ export class SearchResults extends React.Component<PropsType> {
const { data } = row; const { data } = row;
return renderMessageSearchResult(data); return renderMessageSearchResult(data);
} else if (row.type === 'spinner') {
return (
<div className="module-search-results__spinner-container">
<Spinner size="24px" svgSize="small" />
</div>
);
} else { } else {
throw new Error( throw new Error(
'SearchResults.renderRowContents: Encountered unknown row type' 'SearchResults.renderRowContents: Encountered unknown row type'
@ -194,14 +208,24 @@ export class SearchResults extends React.Component<PropsType> {
}; };
public componentDidUpdate(prevProps: PropsType) { public componentDidUpdate(prevProps: PropsType) {
const { items } = this.props; const {
items,
searchTerm,
discussionsLoading,
messagesLoading,
} = this.props;
if ( if (searchTerm !== prevProps.searchTerm) {
this.resizeAll();
} else if (
discussionsLoading !== prevProps.discussionsLoading ||
messagesLoading !== prevProps.messagesLoading
) {
this.resizeAll();
} else if (
items && items &&
items.length > 0 &&
prevProps.items && prevProps.items &&
prevProps.items.length > 0 && prevProps.items.length !== items.length
items !== prevProps.items
) { ) {
this.resizeAll(); this.resizeAll();
} }
@ -270,7 +294,7 @@ export class SearchResults extends React.Component<PropsType> {
} }
return ( return (
<div className="module-search-results" key={searchTerm}> <div className="module-search-results">
<AutoSizer> <AutoSizer>
{({ height, width }) => { {({ height, width }) => {
this.mostRecentWidth = width; this.mostRecentWidth = width;

View file

@ -4,8 +4,8 @@ import { normalize } from '../../types/PhoneNumber';
import { trigger } from '../../shims/events'; import { trigger } from '../../shims/events';
import { cleanSearchTerm } from '../../util/cleanSearchTerm'; import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import { import {
searchConversations, searchConversations as dataSearchConversations,
searchMessages, searchMessages as dataSearchMessages,
searchMessagesInConversation, searchMessagesInConversation,
} from '../../../js/modules/data'; } from '../../../js/modules/data';
import { makeLookup } from '../../util/makeLookup'; import { makeLookup } from '../../util/makeLookup';
@ -40,25 +40,48 @@ export type SearchStateType = {
// We do store message data to pass through the selector // We do store message data to pass through the selector
messageLookup: MessageSearchResultLookupType; messageLookup: MessageSearchResultLookupType;
selectedMessage?: string; selectedMessage?: string;
// Loading state
discussionsLoading: boolean;
messagesLoading: boolean;
}; };
// Actions // Actions
type SearchResultsPayloadType = { type SearchResultsBaseType = {
query: string; query: string;
normalizedPhoneNumber?: string; normalizedPhoneNumber?: string;
};
type SearchMessagesResultsPayloadType = SearchResultsBaseType & {
messages: Array<MessageSearchResultType>; messages: Array<MessageSearchResultType>;
};
type SearchDiscussionsResultsPayloadType = SearchResultsBaseType & {
conversations: Array<string>; conversations: Array<string>;
contacts: Array<string>; contacts: Array<string>;
}; };
type SearchMessagesResultsKickoffActionType = {
type SearchResultsKickoffActionType = { type: 'SEARCH_MESSAGES_RESULTS';
type: 'SEARCH_RESULTS'; payload: Promise<SearchMessagesResultsPayloadType>;
payload: Promise<SearchResultsPayloadType>;
}; };
type SearchResultsFulfilledActionType = { type SearchDiscussionsResultsKickoffActionType = {
type: 'SEARCH_RESULTS_FULFILLED'; type: 'SEARCH_DISCUSSIONS_RESULTS';
payload: SearchResultsPayloadType; payload: Promise<SearchDiscussionsResultsPayloadType>;
};
type SearchMessagesResultsPendingActionType = {
type: 'SEARCH_MESSAGES_RESULTS_PENDING';
payload: SearchMessagesResultsPayloadType;
};
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;
}; };
type UpdateSearchTermActionType = { type UpdateSearchTermActionType = {
type: 'SEARCH_UPDATE'; type: 'SEARCH_UPDATE';
@ -83,7 +106,12 @@ type SearchInConversationActionType = {
}; };
export type SEARCH_TYPES = export type SEARCH_TYPES =
| SearchResultsFulfilledActionType | SearchMessagesResultsKickoffActionType
| SearchDiscussionsResultsKickoffActionType
| SearchMessagesResultsPendingActionType
| SearchDiscussionsResultsPendingActionType
| SearchMessagesResultsFulfilledActionType
| SearchDiscussionsResultsFulfilledActionType
| UpdateSearchTermActionType | UpdateSearchTermActionType
| ClearSearchActionType | ClearSearchActionType
| ClearConversationSearchActionType | ClearConversationSearchActionType
@ -95,7 +123,8 @@ export type SEARCH_TYPES =
// Action Creators // Action Creators
export const actions = { export const actions = {
search, searchMessages,
searchDiscussions,
clearSearch, clearSearch,
clearConversationSearch, clearConversationSearch,
searchInConversation, searchInConversation,
@ -103,59 +132,73 @@ export const actions = {
startNewConversation, startNewConversation,
}; };
function search( function searchMessages(
query: string, query: string,
options: { options: {
searchConversationId?: string;
regionCode: string; regionCode: string;
ourNumber: string;
noteToSelf: string;
} }
): SearchResultsKickoffActionType { ): SearchMessagesResultsKickoffActionType {
return { return {
type: 'SEARCH_RESULTS', type: 'SEARCH_MESSAGES_RESULTS',
payload: doSearch(query, options), payload: doSearchMessages(query, options),
}; };
} }
async function doSearch( function searchDiscussions(
query: string,
options: {
ourNumber: string;
noteToSelf: string;
}
): SearchDiscussionsResultsKickoffActionType {
return {
type: 'SEARCH_DISCUSSIONS_RESULTS',
payload: doSearchDiscussions(query, options),
};
}
async function doSearchMessages(
query: string, query: string,
options: { options: {
searchConversationId?: string; searchConversationId?: string;
regionCode: string; regionCode: string;
ourNumber: string;
noteToSelf: string;
} }
): Promise<SearchResultsPayloadType> { ): Promise<SearchMessagesResultsPayloadType> {
const { regionCode, ourNumber, noteToSelf, searchConversationId } = options; const { regionCode, searchConversationId } = options;
const normalizedPhoneNumber = normalize(query, { regionCode }); const normalizedPhoneNumber = normalize(query, { regionCode });
if (searchConversationId) {
const messages = await queryMessages(query, searchConversationId); const messages = await queryMessages(query, searchConversationId);
return { return {
contacts: [],
conversations: [],
messages, messages,
normalizedPhoneNumber, normalizedPhoneNumber,
query, query,
}; };
} else { }
const [discussions, messages] = await Promise.all([
queryConversationsAndContacts(query, { ourNumber, noteToSelf }), async function doSearchDiscussions(
queryMessages(query), query: string,
]); options: {
const { conversations, contacts } = discussions; ourNumber: string;
noteToSelf: string;
}
): Promise<SearchDiscussionsResultsPayloadType> {
const { ourNumber, noteToSelf } = options;
const { conversations, contacts } = await queryConversationsAndContacts(
query,
{
ourNumber,
noteToSelf,
}
);
return { return {
contacts,
conversations, conversations,
messages, contacts,
normalizedPhoneNumber,
query, query,
}; };
} }
}
function clearSearch(): ClearSearchActionType { function clearSearch(): ClearSearchActionType {
return { return {
type: 'SEARCH_CLEAR', type: 'SEARCH_CLEAR',
@ -214,7 +257,7 @@ async function queryMessages(query: string, searchConversationId?: string) {
return searchMessagesInConversation(normalized, searchConversationId); return searchMessagesInConversation(normalized, searchConversationId);
} }
return searchMessages(normalized); return dataSearchMessages(normalized);
} catch (e) { } catch (e) {
return []; return [];
} }
@ -227,7 +270,7 @@ async function queryConversationsAndContacts(
const { ourNumber, noteToSelf } = options; const { ourNumber, noteToSelf } = options;
const query = providedQuery.replace(/[+-.()]*/g, ''); const query = providedQuery.replace(/[+-.()]*/g, '');
const searchResults: Array<ConversationType> = await searchConversations( const searchResults: Array<ConversationType> = await dataSearchConversations(
query query
); );
@ -266,6 +309,8 @@ function getEmptyState(): SearchStateType {
messageLookup: {}, messageLookup: {},
conversations: [], conversations: [],
contacts: [], contacts: [],
discussionsLoading: false,
messagesLoading: false,
}; };
} }
@ -312,15 +357,27 @@ export function reducer(
}; };
} }
if (action.type === 'SEARCH_RESULTS_FULFILLED') { 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') {
const { payload } = action; const { payload } = action;
const { const { messages, normalizedPhoneNumber, query } = payload;
contacts,
conversations,
messages,
normalizedPhoneNumber,
query,
} = payload;
// Reject if the associated query is not the most recent user-provided query // Reject if the associated query is not the most recent user-provided query
if (state.query !== query) { if (state.query !== query) {
@ -331,12 +388,23 @@ export function reducer(
return { return {
...state, ...state,
contacts,
conversations,
normalizedPhoneNumber, normalizedPhoneNumber,
query, query,
messageIds, messageIds,
messageLookup: makeLookup(messages, 'id'), messageLookup: makeLookup(messages, 'id'),
messagesLoading: false,
};
}
if (action.type === 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED') {
const { payload } = action;
const { contacts, conversations } = payload;
return {
...state,
contacts,
conversations,
discussionsLoading: false,
}; };
} }

View file

@ -63,7 +63,6 @@ export const getMessageSearchResultLookup = createSelector(
getSearch, getSearch,
(state: SearchStateType) => state.messageLookup (state: SearchStateType) => state.messageLookup
); );
export const getSearchResults = createSelector( export const getSearchResults = createSelector(
[getSearch, getRegionCode, getConversationLookup, getSelectedConversation], [getSearch, getRegionCode, getConversationLookup, getSelectedConversation],
( (
@ -71,11 +70,14 @@ export const getSearchResults = createSelector(
regionCode: string, regionCode: string,
lookup: ConversationLookupType, lookup: ConversationLookupType,
selectedConversation?: string selectedConversation?: string
// tslint:disable-next-line max-func-body-length
): SearchResultsPropsType | undefined => { ): SearchResultsPropsType | undefined => {
const { const {
contacts, contacts,
conversations, conversations,
discussionsLoading,
messageIds, messageIds,
messagesLoading,
searchConversationName, searchConversationName,
} = state; } = state;
@ -86,6 +88,8 @@ export const getSearchResults = createSelector(
const haveContacts = contacts && contacts.length; const haveContacts = contacts && contacts.length;
const haveMessages = messageIds && messageIds.length; const haveMessages = messageIds && messageIds.length;
const noResults = const noResults =
!discussionsLoading &&
!messagesLoading &&
!showStartNewConversation && !showStartNewConversation &&
!haveConversations && !haveConversations &&
!haveContacts && !haveContacts &&
@ -115,6 +119,15 @@ export const getSearchResults = createSelector(
}, },
}); });
}); });
} else if (discussionsLoading) {
items.push({
type: 'conversations-header',
data: undefined,
});
items.push({
type: 'spinner',
data: undefined,
});
} }
if (haveContacts) { if (haveContacts) {
@ -145,10 +158,21 @@ export const getSearchResults = createSelector(
data: messageId, data: messageId,
}); });
}); });
} else if (messagesLoading) {
items.push({
type: 'messages-header',
data: undefined,
});
items.push({
type: 'spinner',
data: undefined,
});
} }
return { return {
discussionsLoading,
items, items,
messagesLoading,
noResults, noResults,
regionCode: regionCode, regionCode: regionCode,
searchConversationName, searchConversationName,

View file

@ -7502,7 +7502,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/MainHeader.js", "path": "ts/components/MainHeader.js",
"line": " this.inputRef = react_1.default.createRef();", "line": " this.inputRef = react_1.default.createRef();",
"lineNumber": 83, "lineNumber": 87,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-08-09T21:17:57.798Z", "updated": "2019-08-09T21:17:57.798Z",
"reasonDetail": "Used only to set focus" "reasonDetail": "Used only to set focus"
@ -7511,7 +7511,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/MainHeader.tsx", "path": "ts/components/MainHeader.tsx",
"line": " this.inputRef = React.createRef();", "line": " this.inputRef = React.createRef();",
"lineNumber": 48, "lineNumber": 53,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-08-09T21:17:57.798Z", "updated": "2019-08-09T21:17:57.798Z",
"reasonDetail": "Used only to set focus" "reasonDetail": "Used only to set focus"
@ -7520,7 +7520,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/SearchResults.js", "path": "ts/components/SearchResults.js",
"line": " this.listRef = react_1.default.createRef();", "line": " this.listRef = react_1.default.createRef();",
"lineNumber": 21, "lineNumber": 22,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-08-09T00:44:31.008Z", "updated": "2019-08-09T00:44:31.008Z",
"reasonDetail": "SearchResults needs to interact with its child List directly" "reasonDetail": "SearchResults needs to interact with its child List directly"