Various search UI improvements

This commit is contained in:
Evan Hahn 2021-11-01 13:43:02 -05:00 committed by GitHub
parent 630394d91d
commit a9cb621eb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 835 additions and 577 deletions

View file

@ -1,9 +1,10 @@
// Copyright 2019-2020 Signal Messenger, LLC
// Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { omit, reject } from 'lodash';
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
import { debounce, omit, reject } from 'lodash';
import { normalize } from '../../types/PhoneNumber';
import type { StateType as RootStateType } from '../reducer';
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import type {
ClientSearchResultMessageType,
@ -21,6 +22,8 @@ import type {
SelectedConversationChangedActionType,
ShowArchivedConversationsActionType,
} from './conversations';
import { getQuery, getSearchConversation } from '../selectors/search';
import { getIntl, getUserConversationId } from '../selectors/user';
const {
searchConversations: dataSearchConversations,
@ -41,11 +44,9 @@ export type MessageSearchResultLookupType = {
export type SearchStateType = {
startSearchCounter: number;
searchConversationId?: string;
searchConversationName?: string;
contactIds: Array<string>;
conversationIds: Array<string>;
query: string;
normalizedPhoneNumber?: string;
messageIds: Array<string>;
// We do store message data to pass through the selector
messageLookup: MessageSearchResultLookupType;
@ -57,33 +58,20 @@ export type SearchStateType = {
// Actions
type SearchResultsBaseType = {
query: string;
normalizedPhoneNumber?: string;
};
type SearchMessagesResultsPayloadType = SearchResultsBaseType & {
messages: Array<MessageSearchResultType>;
};
type SearchDiscussionsResultsPayloadType = SearchResultsBaseType & {
conversationIds: Array<string>;
contactIds: Array<string>;
};
type SearchMessagesResultsKickoffActionType = {
type: 'SEARCH_MESSAGES_RESULTS';
payload: Promise<SearchMessagesResultsPayloadType>;
};
type SearchDiscussionsResultsKickoffActionType = {
type: 'SEARCH_DISCUSSIONS_RESULTS';
payload: Promise<SearchDiscussionsResultsPayloadType>;
};
type SearchMessagesResultsFulfilledActionType = {
type: 'SEARCH_MESSAGES_RESULTS_FULFILLED';
payload: SearchMessagesResultsPayloadType;
payload: {
messages: Array<MessageSearchResultType>;
query: string;
};
};
type SearchDiscussionsResultsFulfilledActionType = {
type: 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED';
payload: SearchDiscussionsResultsPayloadType;
payload: {
conversationIds: Array<string>;
contactIds: Array<string>;
query: string;
};
};
type UpdateSearchTermActionType = {
type: 'SEARCH_UPDATE';
@ -105,15 +93,10 @@ type ClearConversationSearchActionType = {
};
type SearchInConversationActionType = {
type: 'SEARCH_IN_CONVERSATION';
payload: {
searchConversationId: string;
searchConversationName: string;
};
payload: { searchConversationId: string };
};
export type SearchActionType =
| SearchMessagesResultsKickoffActionType
| SearchDiscussionsResultsKickoffActionType
| SearchMessagesResultsFulfilledActionType
| SearchDiscussionsResultsFulfilledActionType
| UpdateSearchTermActionType
@ -130,8 +113,6 @@ export type SearchActionType =
// Action Creators
export const actions = {
searchMessages,
searchDiscussions,
startSearch,
clearSearch,
clearConversationSearch,
@ -139,72 +120,6 @@ export const actions = {
updateSearchTerm,
};
function searchMessages(
query: string,
options: {
regionCode: string;
}
): SearchMessagesResultsKickoffActionType {
return {
type: 'SEARCH_MESSAGES_RESULTS',
payload: doSearchMessages(query, options),
};
}
function searchDiscussions(
query: string,
options: {
ourConversationId: string;
noteToSelf: string;
}
): SearchDiscussionsResultsKickoffActionType {
return {
type: 'SEARCH_DISCUSSIONS_RESULTS',
payload: doSearchDiscussions(query, options),
};
}
async function doSearchMessages(
query: string,
options: {
searchConversationId?: string;
regionCode: string;
}
): Promise<SearchMessagesResultsPayloadType> {
const { regionCode, searchConversationId } = options;
const normalizedPhoneNumber = normalize(query, { regionCode });
const messages = await queryMessages(query, searchConversationId);
return {
messages,
normalizedPhoneNumber,
query,
};
}
async function doSearchDiscussions(
query: string,
options: {
ourConversationId: string;
noteToSelf: string;
}
): Promise<SearchDiscussionsResultsPayloadType> {
const { ourConversationId, noteToSelf } = options;
const { conversationIds, contactIds } = await queryConversationsAndContacts(
query,
{
ourConversationId,
noteToSelf,
}
);
return {
conversationIds,
contactIds,
query,
};
}
function startSearch(): StartSearchActionType {
return {
type: 'SEARCH_START',
@ -224,27 +139,92 @@ function clearConversationSearch(): ClearConversationSearchActionType {
};
}
function searchInConversation(
searchConversationId: string,
searchConversationName: string
searchConversationId: string
): SearchInConversationActionType {
return {
type: 'SEARCH_IN_CONVERSATION',
payload: {
searchConversationId,
searchConversationName,
},
payload: { searchConversationId },
};
}
function updateSearchTerm(query: string): UpdateSearchTermActionType {
return {
type: 'SEARCH_UPDATE',
payload: {
query,
},
function updateSearchTerm(
query: string
): ThunkAction<void, RootStateType, unknown, UpdateSearchTermActionType> {
return (dispatch, getState) => {
dispatch({
type: 'SEARCH_UPDATE',
payload: { query },
});
const state = getState();
doSearch({
dispatch,
noteToSelf: getIntl(state)('noteToSelf').toLowerCase(),
ourConversationId: getUserConversationId(state),
query: getQuery(state),
searchConversationId: getSearchConversation(state)?.id,
});
};
}
const doSearch = debounce(
({
dispatch,
noteToSelf,
ourConversationId,
query,
searchConversationId,
}: Readonly<{
dispatch: ThunkDispatch<
RootStateType,
unknown,
| SearchMessagesResultsFulfilledActionType
| SearchDiscussionsResultsFulfilledActionType
>;
noteToSelf: string;
ourConversationId: string;
query: string;
searchConversationId: undefined | string;
}>) => {
if (!query) {
return;
}
(async () => {
dispatch({
type: 'SEARCH_MESSAGES_RESULTS_FULFILLED',
payload: {
messages: await queryMessages(query, searchConversationId),
query,
},
});
})();
if (!searchConversationId) {
(async () => {
const {
conversationIds,
contactIds,
} = await queryConversationsAndContacts(query, {
ourConversationId,
noteToSelf,
});
dispatch({
type: 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED',
payload: {
conversationIds,
contactIds,
query,
},
});
})();
}
},
200
);
async function queryMessages(
query: string,
searchConversationId?: string
@ -342,7 +322,6 @@ export function reducer(
return {
...state,
searchConversationId: undefined,
searchConversationName: undefined,
startSearchCounter: state.startSearchCounter + 1,
};
}
@ -376,7 +355,7 @@ export function reducer(
if (action.type === 'SEARCH_IN_CONVERSATION') {
const { payload } = action;
const { searchConversationId, searchConversationName } = payload;
const { searchConversationId } = payload;
if (searchConversationId === state.searchConversationId) {
return {
@ -388,23 +367,21 @@ export function reducer(
return {
...getEmptyState(),
searchConversationId,
searchConversationName,
startSearchCounter: state.startSearchCounter + 1,
};
}
if (action.type === 'CLEAR_CONVERSATION_SEARCH') {
const { searchConversationId, searchConversationName } = state;
const { searchConversationId } = state;
return {
...getEmptyState(),
searchConversationId,
searchConversationName,
};
}
if (action.type === 'SEARCH_MESSAGES_RESULTS_FULFILLED') {
const { payload } = action;
const { messages, normalizedPhoneNumber, query } = payload;
const { messages, query } = payload;
// Reject if the associated query is not the most recent user-provided query
if (state.query !== query) {
@ -415,7 +392,6 @@ export function reducer(
return {
...state,
normalizedPhoneNumber,
query,
messageIds,
messageLookup: makeLookup(messages, 'id'),
@ -425,7 +401,12 @@ export function reducer(
if (action.type === 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED') {
const { payload } = action;
const { contactIds, conversationIds } = payload;
const { contactIds, conversationIds, query } = payload;
// Reject if the associated query is not the most recent user-provided query
if (state.query !== query) {
return state;
}
return {
...state,

View file

@ -21,7 +21,7 @@ import type {
import type { LeftPaneSearchPropsType } from '../../components/leftPane/LeftPaneSearchHelper';
import type { PropsDataType as MessageSearchResultPropsDataType } from '../../components/conversationList/MessageSearchResult';
import { getUserConversationId } from './user';
import { getIntl, getUserConversationId } from './user';
import type { GetConversationByIdType } from './conversations';
import {
getConversationLookup,
@ -30,6 +30,7 @@ import {
import type { BodyRangeType } from '../../types/Util';
import * as log from '../../logging/log';
import { getOwn } from '../../util/getOwn';
export const getSearch = (state: StateType): SearchStateType => state.search;
@ -43,7 +44,7 @@ export const getSelectedMessage = createSelector(
(state: SearchStateType): string | undefined => state.selectedMessage
);
export const getSearchConversationId = createSelector(
const getSearchConversationId = createSelector(
getSearch,
(state: SearchStateType): string | undefined => state.searchConversationId
);
@ -53,9 +54,24 @@ export const getIsSearchingInAConversation = createSelector(
Boolean
);
export const getSearchConversation = createSelector(
getSearchConversationId,
getConversationLookup,
(searchConversationId, conversationLookup): undefined | ConversationType =>
searchConversationId
? getOwn(conversationLookup, searchConversationId)
: undefined
);
export const getSearchConversationName = createSelector(
getSearch,
(state: SearchStateType): string | undefined => state.searchConversationName
getSearchConversation,
getIntl,
(conversation, i18n): undefined | string => {
if (!conversation) {
return undefined;
}
return conversation.isMe ? i18n('noteToSelf') : conversation.title;
}
);
export const getStartSearchCounter = createSelector(
@ -74,9 +90,10 @@ export const getMessageSearchResultLookup = createSelector(
);
export const getSearchResults = createSelector(
[getSearch, getConversationLookup],
[getSearch, getSearchConversationName, getConversationLookup],
(
state: SearchStateType,
searchConversationName,
conversationLookup: ConversationLookupType
): Omit<LeftPaneSearchPropsType, 'primarySendsSms'> => {
const {
@ -86,7 +103,6 @@ export const getSearchResults = createSelector(
messageIds,
messageLookup,
messagesLoading,
searchConversationName,
} = state;
return {

View file

@ -13,6 +13,8 @@ import { missingCaseError } from '../../util/missingCaseError';
import { ComposerStep, OneTimeModalState } from '../ducks/conversations';
import {
getIsSearchingInAConversation,
getQuery,
getSearchConversation,
getSearchResults,
getStartSearchCounter,
isSearching,
@ -91,9 +93,14 @@ const getModeSpecificProps = (
case undefined:
if (getShowArchived(state)) {
const { archivedConversations } = getLeftPaneLists(state);
const searchConversation = getSearchConversation(state);
const searchTerm = getQuery(state);
return {
mode: LeftPaneMode.Archive,
archivedConversations,
searchConversation,
searchTerm,
...(searchConversation && searchTerm ? getSearchResults(state) : {}),
};
}
if (isSearching(state)) {

View file

@ -1,4 +1,4 @@
// Copyright 2019-2020 Signal Messenger, LLC
// Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
@ -9,8 +9,7 @@ import type { StateType } from '../reducer';
import {
getQuery,
getSearchConversationId,
getSearchConversationName,
getSearchConversation,
getStartSearchCounter,
} from '../selectors/search';
import {
@ -27,8 +26,7 @@ const mapStateToProps = (state: StateType) => {
disabled: state.network.challengeStatus !== 'idle',
hasPendingUpdate: Boolean(state.updates.didSnooze),
searchTerm: getQuery(state),
searchConversationId: getSearchConversationId(state),
searchConversationName: getSearchConversationName(state),
searchConversation: getSearchConversation(state),
selectedConversation: getSelectedConversation(state),
startSearchCounter: getStartSearchCounter(state),
regionCode: getRegionCode(state),