signal-desktop/ts/state/selectors/search.ts

258 lines
7.3 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2019 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import memoizee from 'memoizee';
2019-01-14 21:49:58 +00:00
import { createSelector } from 'reselect';
import { deconstructLookup } from '../../util/deconstructLookup';
2019-01-14 21:49:58 +00:00
import type { StateType } from '../reducer';
2019-01-14 21:49:58 +00:00
import type {
MessageSearchResultLookupType,
MessageSearchResultType,
SearchStateType,
} from '../ducks/search';
import type {
ConversationLookupType,
ConversationType,
} from '../ducks/conversations';
import type { LeftPaneSearchPropsType } from '../../components/leftPane/LeftPaneSearchHelper';
import type { PropsDataType as MessageSearchResultPropsDataType } from '../../components/conversationList/MessageSearchResult';
2021-11-01 18:43:02 +00:00
import { getIntl, getUserConversationId } from './user';
import type { GetConversationByIdType } from './conversations';
import {
2019-01-14 21:49:58 +00:00
getConversationLookup,
getConversationSelector,
2019-01-14 21:49:58 +00:00
} from './conversations';
2019-03-12 00:20:16 +00:00
import { hydrateRanges } from '../../types/BodyRange';
import * as log from '../../logging/log';
2021-11-01 18:43:02 +00:00
import { getOwn } from '../../util/getOwn';
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,
2023-03-20 22:23:53 +00:00
(state: SearchStateType): string | undefined => state.targetedMessage
2019-01-14 21:49:58 +00:00
);
2021-11-01 18:43:02 +00:00
const getSearchConversationId = createSelector(
2019-08-09 23:12:29 +00:00
getSearch,
(state: SearchStateType): string | undefined => state.searchConversationId
);
export const getIsSearchingInAConversation = createSelector(
getSearchConversationId,
Boolean
);
export const getIsSearchingGlobally = createSelector(
getSearch,
(state: SearchStateType): boolean => Boolean(state.globalSearch)
);
export const getIsSearching = createSelector(
getIsSearchingInAConversation,
getIsSearchingGlobally,
(isSearchingInAConversation, isSearchingGlobally): boolean =>
isSearchingInAConversation || isSearchingGlobally
);
2021-11-01 18:43:02 +00:00
export const getSearchConversation = createSelector(
getSearchConversationId,
getConversationLookup,
(searchConversationId, conversationLookup): undefined | ConversationType =>
searchConversationId
? getOwn(conversationLookup, searchConversationId)
: undefined
);
2019-08-09 23:12:29 +00:00
export const getSearchConversationName = createSelector(
2021-11-01 18:43:02 +00:00
getSearchConversation,
getIntl,
(conversation, i18n): undefined | string => {
if (!conversation) {
return undefined;
}
2023-03-30 00:03:25 +00:00
return conversation.isMe ? i18n('icu:noteToSelf') : conversation.title;
2021-11-01 18:43:02 +00:00
}
2019-08-09 23:12:29 +00:00
);
2019-11-07 21:36:16 +00:00
export const getStartSearchCounter = createSelector(
getSearch,
(state: SearchStateType): number => state.startSearchCounter
);
export const getHasSearchQuery = createSelector(
2021-03-18 20:36:47 +00:00
getQuery,
(query: string): boolean => query.trim().length > 0
2019-01-14 21:49:58 +00:00
);
export const getMessageSearchResultLookup = createSelector(
getSearch,
(state: SearchStateType) => state.messageLookup
);
2019-01-14 21:49:58 +00:00
export const getSearchResults = createSelector(
2021-11-01 18:43:02 +00:00
[getSearch, getSearchConversationName, getConversationLookup],
2019-01-14 21:49:58 +00:00
(
state: SearchStateType,
2021-11-01 18:43:02 +00:00
searchConversationName,
conversationLookup: ConversationLookupType
2022-01-27 22:12:26 +00:00
): Pick<
LeftPaneSearchPropsType,
| 'conversationResults'
| 'contactResults'
| 'messageResults'
| 'searchConversationName'
| 'searchTerm'
> => {
2019-08-09 23:12:29 +00:00
const {
contactIds,
conversationIds,
discussionsLoading,
2019-08-09 23:12:29 +00:00
messageIds,
messageLookup,
messagesLoading,
2019-08-09 23:12:29 +00:00
} = state;
return {
conversationResults: discussionsLoading
? { isLoading: true }
: {
isLoading: false,
results: deconstructLookup(conversationLookup, conversationIds),
},
contactResults: discussionsLoading
? { isLoading: true }
: {
isLoading: false,
results: deconstructLookup(conversationLookup, contactIds),
},
messageResults: messagesLoading
? { isLoading: true }
: {
isLoading: false,
results: deconstructLookup(messageLookup, messageIds),
},
2019-08-09 23:12:29 +00:00
searchConversationName,
2019-01-14 21:49:58 +00:00
searchTerm: state.query,
};
}
);
// A little optimization to reset our selector cache whenever high-level application data
// changes: regionCode and userNumber.
type CachedMessageSearchResultSelectorType = (
message: MessageSearchResultType,
from: ConversationType,
to: ConversationType,
2019-08-09 23:12:29 +00:00
searchConversationId?: string,
2023-03-20 22:23:53 +00:00
targetedMessageId?: string
) => MessageSearchResultPropsDataType;
export const getCachedSelectorForMessageSearchResult = createSelector(
getUserConversationId,
getConversationSelector,
(
_,
conversationSelector: GetConversationByIdType
): CachedMessageSearchResultSelectorType => {
// Note: memoizee will check all parameters provided, and only run our selector
// if any of them have changed.
return memoizee(
(
message: MessageSearchResultType,
from: ConversationType,
to: ConversationType,
searchConversationId?: string,
2023-03-20 22:23:53 +00:00
targetedMessageId?: string
) => {
return {
from,
to,
id: message.id,
conversationId: message.conversationId,
sentAt: message.sent_at,
snippet: message.snippet || '',
bodyRanges:
hydrateRanges(message.bodyRanges, conversationSelector) || [],
body: message.body || '',
isSelected: Boolean(
2023-03-20 22:23:53 +00:00
targetedMessageId && message.id === targetedMessageId
),
isSearchingInConversation: Boolean(searchConversationId),
};
},
{ 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,
getUserConversationId,
(
messageSearchResultSelector: CachedMessageSearchResultSelectorType,
messageSearchResultLookup: MessageSearchResultLookupType,
2023-03-20 22:23:53 +00:00
targetedMessageId: string | undefined,
conversationSelector: GetConversationByIdType,
2019-08-09 23:12:29 +00:00
searchConversationId: string | undefined,
ourConversationId: string | undefined
): GetMessageSearchResultByIdType => {
return (id: string) => {
const message = messageSearchResultLookup[id];
if (!message) {
log.warn(
`getMessageSearchResultSelector: messageSearchResultLookup was missing id ${id}`
);
return undefined;
}
2023-08-16 20:54:39 +00:00
const { conversationId, source, sourceServiceId, type } = message;
let from: ConversationType;
let to: ConversationType;
if (type === 'incoming') {
2023-08-16 20:54:39 +00:00
from = conversationSelector(sourceServiceId || source);
to = conversationSelector(conversationId);
if (from === to) {
to = conversationSelector(ourConversationId);
}
} else if (type === 'outgoing') {
from = conversationSelector(ourConversationId);
to = conversationSelector(conversationId);
} else {
log.warn(`getMessageSearchResultSelector: Got unexpected type ${type}`);
return undefined;
}
return messageSearchResultSelector(
message,
from,
to,
2019-08-09 23:12:29 +00:00
searchConversationId,
2023-03-20 22:23:53 +00:00
targetedMessageId
);
2019-01-14 21:49:58 +00:00
};
}
);