Add "new conversation" composer for direct messages

This commit is contained in:
Evan Hahn 2021-02-23 14:34:28 -06:00 committed by Josh Perez
parent 84dc166b63
commit 06fb4fd0bc
61 changed files with 5960 additions and 3887 deletions

View file

@ -18,8 +18,8 @@ import {
import { StateType as RootStateType } from '../reducer';
import { calling } from '../../services/calling';
import { getOwn } from '../../util/getOwn';
import { assert } from '../../util/assert';
import { trigger } from '../../shims/events';
import { NoopActionType } from './noop';
import { AttachmentType } from '../../types/Attachment';
import { ColorType } from '../../types/Colors';
import { BodyRangeType } from '../../types/Util';
@ -65,6 +65,7 @@ export type ConversationType = {
canChangeTimer?: boolean;
canEditGroupInfo?: boolean;
color?: ColorType;
discoveredUnregisteredAt?: number;
isAccepted?: boolean;
isArchived?: boolean;
isBlocked?: boolean;
@ -110,6 +111,7 @@ export type ConversationType = {
profileName?: string;
} | null;
recentMediaItems?: Array<MediaItemType>;
profileSharing?: boolean;
shouldShowDraft?: boolean;
draftText?: string | null;
@ -120,7 +122,6 @@ export type ConversationType = {
groupVersion?: 1 | 2;
groupId?: string;
groupLink?: string;
isMissingMandatoryProfileSharing?: boolean;
messageRequestsEnabled?: boolean;
acceptedMessageRequest?: boolean;
secretParams?: string;
@ -231,6 +232,9 @@ export type ConversationsStateType = {
selectedConversationTitle?: string;
selectedConversationPanelDepth: number;
showArchived: boolean;
composer?: {
contactSearchTerm: string;
};
// Note: it's very important that both of these locations are always kept up to date
messagesLookup: MessageLookupType;
@ -431,6 +435,10 @@ export type ShowArchivedConversationsActionType = {
type: 'SHOW_ARCHIVED_CONVERSATIONS';
payload: null;
};
type SetComposeSearchTermActionType = {
type: 'SET_COMPOSE_SEARCH_TERM';
payload: { contactSearchTerm: string };
};
type SetRecentMediaItemsActionType = {
type: 'SET_RECENT_MEDIA_ITEMS';
payload: {
@ -438,6 +446,13 @@ type SetRecentMediaItemsActionType = {
recentMediaItems: Array<MediaItemType>;
};
};
type StartComposingActionType = {
type: 'START_COMPOSING';
};
export type SwitchToAssociatedViewActionType = {
type: 'SWITCH_TO_ASSOCIATED_VIEW';
payload: { conversationId: string };
};
export type ConversationActionType =
| ClearChangedMessagesActionType
@ -458,6 +473,7 @@ export type ConversationActionType =
| RepairOldestMessageActionType
| ScrollToMessageActionType
| SelectedConversationChangedActionType
| SetComposeSearchTermActionType
| SetConversationHeaderTitleActionType
| SetIsNearBottomActionType
| SetLoadCountdownStartActionType
@ -466,7 +482,9 @@ export type ConversationActionType =
| SetRecentMediaItemsActionType
| SetSelectedConversationPanelDepthActionType
| ShowArchivedConversationsActionType
| ShowInboxActionType;
| ShowInboxActionType
| StartComposingActionType
| SwitchToAssociatedViewActionType;
// Action Creators
@ -490,6 +508,7 @@ export const actions = {
repairOldestMessage,
scrollToMessage,
selectMessage,
setComposeSearchTerm,
setIsNearBottom,
setLoadCountdownStart,
setMessagesLoading,
@ -499,6 +518,8 @@ export const actions = {
setSelectedConversationPanelDepth,
showArchivedConversations,
showInbox,
startComposing,
startNewConversationFromPhoneNumber,
};
function setPreJoinConversation(
@ -770,19 +791,56 @@ function scrollToMessage(
};
}
function setComposeSearchTerm(
contactSearchTerm: string
): SetComposeSearchTermActionType {
return {
type: 'SET_COMPOSE_SEARCH_TERM',
payload: { contactSearchTerm },
};
}
function startComposing(): StartComposingActionType {
return { type: 'START_COMPOSING' };
}
function startNewConversationFromPhoneNumber(
e164: string
): ThunkAction<void, RootStateType, unknown, ShowInboxActionType> {
return dispatch => {
trigger('showConversation', e164);
dispatch(showInbox());
};
}
// Note: we need two actions here to simplify. Operations outside of the left pane can
// trigger an 'openConversation' so we go through Whisper.events for all
// conversation selection. Internal just triggers the Whisper.event, and External
// makes the changes to the store.
function openConversationInternal(
id: string,
messageId?: string
): NoopActionType {
trigger('showConversation', id, messageId);
function openConversationInternal({
conversationId,
messageId,
switchToAssociatedView,
}: Readonly<{
conversationId: string;
messageId?: string;
switchToAssociatedView?: boolean;
}>): ThunkAction<
void,
RootStateType,
unknown,
SwitchToAssociatedViewActionType
> {
return dispatch => {
trigger('showConversation', conversationId, messageId);
return {
type: 'NOOP',
payload: null,
if (switchToAssociatedView) {
dispatch({
type: 'SWITCH_TO_ASSOCIATED_VIEW',
payload: { conversationId },
});
}
};
}
function openConversationExternal(
@ -1626,13 +1684,13 @@ export function reducer(
}
if (action.type === 'SHOW_INBOX') {
return {
...state,
...omit(state, 'composer'),
showArchived: false,
};
}
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
return {
...state,
...omit(state, 'composer'),
showArchived: true,
};
}
@ -1669,5 +1727,52 @@ export function reducer(
};
}
if (action.type === 'START_COMPOSING') {
if (state.composer) {
return state;
}
return {
...state,
showArchived: false,
composer: {
contactSearchTerm: '',
},
};
}
if (action.type === 'SET_COMPOSE_SEARCH_TERM') {
const { composer } = state;
if (!composer) {
assert(
false,
'Setting compose search term with the composer closed is a no-op'
);
return state;
}
return {
...state,
composer: {
...composer,
contactSearchTerm: action.payload.contactSearchTerm,
},
};
}
if (action.type === 'SWITCH_TO_ASSOCIATED_VIEW') {
const conversation = getOwn(
state.conversationLookup,
action.payload.conversationId
);
if (!conversation) {
return state;
}
return {
...omit(state, 'composer'),
showArchived: Boolean(conversation.isArchived),
};
}
return state;
}

View file

@ -4,7 +4,6 @@
import { omit, reject } from 'lodash';
import { normalize } from '../../types/PhoneNumber';
import { trigger } from '../../shims/events';
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import dataInterface from '../../sql/Client';
import { makeLookup } from '../../util/makeLookup';
@ -39,9 +38,8 @@ export type SearchStateType = {
startSearchCounter: number;
searchConversationId?: string;
searchConversationName?: string;
// We store just ids of conversations, since that data is always cached in memory
contacts: Array<string>;
conversations: Array<string>;
contactIds: Array<string>;
conversationIds: Array<string>;
query: string;
normalizedPhoneNumber?: string;
messageIds: Array<string>;
@ -63,8 +61,8 @@ type SearchMessagesResultsPayloadType = SearchResultsBaseType & {
messages: Array<MessageSearchResultType>;
};
type SearchDiscussionsResultsPayloadType = SearchResultsBaseType & {
conversations: Array<string>;
contacts: Array<string>;
conversationIds: Array<string>;
contactIds: Array<string>;
};
type SearchMessagesResultsKickoffActionType = {
type: 'SEARCH_MESSAGES_RESULTS';
@ -135,7 +133,6 @@ export const actions = {
clearConversationSearch,
searchInConversation,
updateSearchTerm,
startNewConversation,
};
function searchMessages(
@ -190,7 +187,7 @@ async function doSearchDiscussions(
}
): Promise<SearchDiscussionsResultsPayloadType> {
const { ourConversationId, noteToSelf } = options;
const { conversations, contacts } = await queryConversationsAndContacts(
const { conversationIds, contactIds } = await queryConversationsAndContacts(
query,
{
ourConversationId,
@ -199,8 +196,8 @@ async function doSearchDiscussions(
);
return {
conversations,
contacts,
conversationIds,
contactIds,
query,
};
}
@ -243,22 +240,6 @@ function updateSearchTerm(query: string): UpdateSearchTermActionType {
},
};
}
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,
};
}
async function queryMessages(query: string, searchConversationId?: string) {
try {
@ -280,7 +261,10 @@ async function queryConversationsAndContacts(
ourConversationId: string;
noteToSelf: string;
}
) {
): Promise<{
contactIds: Array<string>;
conversationIds: Array<string>;
}> {
const { ourConversationId, noteToSelf } = options;
const query = providedQuery.replace(/[+.()]*/g, '');
@ -289,16 +273,16 @@ async function queryConversationsAndContacts(
);
// Split into two groups - active conversations and items just from address book
let conversations: Array<string> = [];
let contacts: Array<string> = [];
let conversationIds: Array<string> = [];
let contactIds: Array<string> = [];
const max = searchResults.length;
for (let i = 0; i < max; i += 1) {
const conversation = searchResults[i];
if (conversation.type === 'private' && !conversation.lastMessage) {
contacts.push(conversation.id);
contactIds.push(conversation.id);
} else {
conversations.push(conversation.id);
conversationIds.push(conversation.id);
}
}
@ -312,13 +296,13 @@ async function queryConversationsAndContacts(
// 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 !== ourConversationId);
conversations = conversations.filter(id => id !== ourConversationId);
contactIds = contactIds.filter(id => id !== ourConversationId);
conversationIds = conversationIds.filter(id => id !== ourConversationId);
contacts.unshift(ourConversationId);
contactIds.unshift(ourConversationId);
}
return { conversations, contacts };
return { conversationIds, contactIds };
}
// Reducer
@ -329,8 +313,8 @@ export function getEmptyState(): SearchStateType {
query: '',
messageIds: [],
messageLookup: {},
conversations: [],
contacts: [],
conversationIds: [],
contactIds: [],
discussionsLoading: false,
messagesLoading: false,
};
@ -373,8 +357,8 @@ export function reducer(
messageIds: [],
messageLookup: {},
discussionsLoading: !isWithinConversation,
contacts: [],
conversations: [],
contactIds: [],
conversationIds: [],
}
: {}),
};
@ -431,12 +415,12 @@ export function reducer(
if (action.type === 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED') {
const { payload } = action;
const { contacts, conversations } = payload;
const { contactIds, conversationIds } = payload;
return {
...state,
contacts,
conversations,
contactIds,
conversationIds,
discussionsLoading: false,
};
}

View file

@ -2,8 +2,9 @@
// SPDX-License-Identifier: AGPL-3.0-only
import memoizee from 'memoizee';
import { fromPairs, isNumber } from 'lodash';
import { fromPairs, isNumber, isString } from 'lodash';
import { createSelector } from 'reselect';
import Fuse, { FuseOptions } from 'fuse.js';
import { StateType } from '../reducer';
import {
@ -16,6 +17,7 @@ import {
MessageType,
PreJoinConversationType,
} from '../ducks/conversations';
import { LocalizerType } from '../../types/Util';
import { getOwn } from '../../util/getOwn';
import type { CallsByConversationType } from '../ducks/calling';
import { getCallsByConversation } from './calling';
@ -23,6 +25,7 @@ import { getBubbleProps } from '../../shims/Whisper';
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
import { TimelineItemType } from '../../components/conversation/TimelineItem';
import { assert } from '../../util/assert';
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import {
getInteractionMode,
@ -135,6 +138,16 @@ export const getShowArchived = createSelector(
}
);
const getComposerState = createSelector(
getConversations,
(state: ConversationsStateType) => state.composer
);
export const isComposing = createSelector(
getComposerState,
(composerState): boolean => Boolean(composerState)
);
export const getMessages = createSelector(
getConversations,
(state: ConversationsStateType): MessageLookupType => {
@ -148,6 +161,20 @@ export const getMessagesByConversation = createSelector(
}
);
export const getIsConversationEmptySelector = createSelector(
getMessagesByConversation,
(messagesByConversation: MessagesByConversationType) => (
conversationId: string
): boolean => {
const messages = getOwn(messagesByConversation, conversationId);
if (!messages) {
assert(false, 'Could not find conversation with this ID');
return true;
}
return messages.messageIds.length === 0;
}
);
const collator = new Intl.Collator();
// Note: we will probably want to put i18n and regionCode back when we are formatting
@ -256,6 +283,86 @@ export const getMe = createSelector(
}
);
export const getComposerContactSearchTerm = createSelector(
getComposerState,
(composer): string => {
if (!composer) {
assert(false, 'getComposerContactSearchTerm: composer is not open');
return '';
}
return composer.contactSearchTerm;
}
);
/**
* This returns contacts for the composer, which isn't just your primary's system
* contacts. It may include false positives, which is better than missing contacts.
*
* Because it filters unregistered contacts and that's (partially) determined by the
* current time, it's possible for this to return stale contacts that have unregistered
* if no other conversations change. This should be a rare false positive.
*/
const getContacts = createSelector(
getConversationLookup,
(conversationLookup: ConversationLookupType): Array<ConversationType> =>
Object.values(conversationLookup).filter(
contact =>
contact.type === 'direct' &&
!contact.isMe &&
!contact.isBlocked &&
!isConversationUnregistered(contact) &&
(isString(contact.name) || contact.profileSharing)
)
);
const getNormalizedComposerContactSearchTerm = createSelector(
getComposerContactSearchTerm,
(searchTerm: string): string => searchTerm.trim()
);
const getNoteToSelfTitle = createSelector(getIntl, (i18n: LocalizerType) =>
i18n('noteToSelf').toLowerCase()
);
const COMPOSE_CONTACTS_FUSE_OPTIONS: FuseOptions<ConversationType> = {
// A small-but-nonzero threshold lets us match parts of E164s better, and makes the
// search a little more forgiving.
threshold: 0.05,
keys: ['title', 'name', 'e164'],
};
export const getComposeContacts = createSelector(
getNormalizedComposerContactSearchTerm,
getContacts,
getMe,
getNoteToSelfTitle,
(
searchTerm: string,
contacts: Array<ConversationType>,
noteToSelf: ConversationType,
noteToSelfTitle: string
): Array<ConversationType> => {
let result: Array<ConversationType>;
if (searchTerm.length) {
const fuse = new Fuse<ConversationType>(
contacts,
COMPOSE_CONTACTS_FUSE_OPTIONS
);
result = fuse.search(searchTerm);
if (noteToSelfTitle.includes(searchTerm)) {
result.push(noteToSelf);
}
} else {
result = contacts.concat();
result.sort((a, b) => collator.compare(a.title, b.title));
result.push(noteToSelf);
}
return result;
}
);
// This is where we will put Conversation selector logic, replicating what
// is currently in models/conversation.getProps()
// What needs to happen to pull that selector logic here?

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 memoizee from 'memoizee';
import { createSelector } from 'reselect';
import { instance } from '../../util/libphonenumberInstance';
import { deconstructLookup } from '../../util/deconstructLookup';
import { StateType } from '../reducer';
@ -17,19 +18,14 @@ import {
ConversationType,
} from '../ducks/conversations';
import {
PropsDataType as SearchResultsPropsType,
SearchResultRowType,
} from '../../components/SearchResults';
import { PropsDataType as MessageSearchResultPropsDataType } from '../../components/MessageSearchResult';
import { LeftPaneSearchPropsType } from '../../components/leftPane/LeftPaneSearchHelper';
import { PropsDataType as MessageSearchResultPropsDataType } from '../../components/conversationList/MessageSearchResult';
import { getRegionCode, getUserConversationId } from './user';
import { getUserAgent } from './items';
import { getUserConversationId } from './user';
import {
GetConversationByIdType,
getConversationLookup,
getConversationSelector,
getSelectedConversationId,
} from './conversations';
export const getSearch = (state: StateType): SearchStateType => state.search;
@ -72,148 +68,44 @@ export const getMessageSearchResultLookup = createSelector(
getSearch,
(state: SearchStateType) => state.messageLookup
);
export const getSearchResults = createSelector(
[
getSearch,
getRegionCode,
getUserAgent,
getConversationLookup,
getSelectedConversationId,
getSelectedMessage,
],
[getSearch, getConversationLookup],
(
state: SearchStateType,
regionCode: string,
userAgent: string,
lookup: ConversationLookupType,
selectedConversationId?: string,
selectedMessageId?: string
): SearchResultsPropsType | undefined => {
conversationLookup: ConversationLookupType
): LeftPaneSearchPropsType => {
const {
contacts,
conversations,
contactIds,
conversationIds,
discussionsLoading,
messageIds,
messageLookup,
messagesLoading,
searchConversationName,
} = state;
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 =
!discussionsLoading &&
!messagesLoading &&
!showStartNewConversation &&
!haveConversations &&
!haveContacts &&
!haveMessages;
const items: Array<SearchResultRowType> = [];
if (showStartNewConversation) {
items.push({
type: 'start-new-conversation',
data: undefined,
});
const isIOS = userAgent === 'OWI';
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
}
if (!isIOS && isValidNumber) {
items.push({
type: 'sms-mms-not-supported-text',
data: undefined,
});
}
}
if (haveConversations) {
items.push({
type: 'conversations-header',
data: undefined,
});
conversations.forEach(id => {
const data = lookup[id];
items.push({
type: 'conversation',
data: {
...data,
isSelected: Boolean(data && id === selectedConversationId),
},
});
});
} else if (discussionsLoading) {
items.push({
type: 'conversations-header',
data: undefined,
});
items.push({
type: 'spinner',
data: undefined,
});
}
if (haveContacts) {
items.push({
type: 'contacts-header',
data: undefined,
});
contacts.forEach(id => {
const data = lookup[id];
items.push({
type: 'contact',
data: {
...data,
isSelected: Boolean(data && id === selectedConversationId),
},
});
});
}
if (haveMessages) {
items.push({
type: 'messages-header',
data: undefined,
});
messageIds.forEach(messageId => {
items.push({
type: 'message',
data: messageId,
});
});
} else if (messagesLoading) {
items.push({
type: 'messages-header',
data: undefined,
});
items.push({
type: 'spinner',
data: undefined,
});
}
return {
discussionsLoading,
items,
messagesLoading,
noResults,
regionCode,
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),
},
searchConversationName,
searchTerm: state.query,
selectedConversationId,
selectedMessageId,
};
}
);

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';
@ -10,7 +10,10 @@ import { StateType } from '../reducer';
import { isShortName } from '../../components/emoji/lib';
import { getIntl } from '../selectors/user';
import { getConversationSelector } from '../selectors/conversations';
import {
getConversationSelector,
getIsConversationEmptySelector,
} from '../selectors/conversations';
import {
getBlessedStickerPacks,
getInstalledStickerPacks,
@ -78,6 +81,10 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
// Message Requests
...conversation,
conversationType: conversation.type,
isMissingMandatoryProfileSharing:
!conversation.profileSharing &&
window.Signal.RemoteConfig.isEnabled('desktop.mandatoryProfileSharing') &&
!getIsConversationEmptySelector(state)(id),
};
};

View file

@ -7,7 +7,10 @@ import {
ConversationHeader,
OutgoingCallButtonStyle,
} from '../../components/conversation/ConversationHeader';
import { getConversationSelector } from '../selectors/conversations';
import {
getConversationSelector,
getIsConversationEmptySelector,
} from '../selectors/conversations';
import { StateType } from '../reducer';
import { CallMode } from '../../types/Calling';
import {
@ -78,7 +81,9 @@ const getOutgoingCallButtonStyle = (
};
const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
const conversation = getConversationSelector(state)(ownProps.id);
const { id } = ownProps;
const conversation = getConversationSelector(state)(id);
if (!conversation) {
throw new Error('Could not find conversation');
}
@ -92,7 +97,6 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
'expireTimer',
'isArchived',
'isMe',
'isMissingMandatoryProfileSharing',
'isPinned',
'isVerified',
'left',
@ -106,6 +110,10 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
'groupVersion',
]),
conversationTitle: state.conversations.selectedConversationTitle,
isMissingMandatoryProfileSharing:
!conversation.profileSharing &&
window.Signal.RemoteConfig.isEnabled('desktop.mandatoryProfileSharing') &&
!getIsConversationEmptySelector(state)(id),
i18n: getIntl(state),
showBackButton: state.conversations.selectedConversationPanelDepth > 0,
outgoingCallButtonStyle: getOutgoingCallButtonStyle(conversation, state),

View file

@ -1,18 +1,26 @@
// Copyright 2019-2020 Signal Messenger, LLC
// Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { CSSProperties } from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { LeftPane } from '../../components/LeftPane';
import {
LeftPane,
LeftPaneMode,
PropsType as LeftPanePropsType,
} from '../../components/LeftPane';
import { StateType } from '../reducer';
import { getSearchResults, isSearching } from '../selectors/search';
import { getIntl } from '../selectors/user';
import { getIntl, getRegionCode } from '../selectors/user';
import {
getComposeContacts,
getComposerContactSearchTerm,
getLeftPaneLists,
getSelectedConversationId,
getSelectedMessage,
getShowArchived,
isComposing,
} from '../selectors/conversations';
import { SmartExpiredBuildDialog } from './ExpiredBuildDialog';
@ -34,8 +42,11 @@ function renderExpiredBuildDialog(): JSX.Element {
function renderMainHeader(): JSX.Element {
return <SmartMainHeader />;
}
function renderMessageSearchResult(id: string): JSX.Element {
return <FilteredSmartMessageSearchResult id={id} />;
function renderMessageSearchResult(
id: string,
style: CSSProperties
): JSX.Element {
return <FilteredSmartMessageSearchResult id={id} style={style} />;
}
function renderNetworkStatus(): JSX.Element {
return <SmartNetworkStatus />;
@ -47,19 +58,47 @@ function renderUpdateDialog(): JSX.Element {
return <SmartUpdateDialog />;
}
const mapStateToProps = (state: StateType) => {
const showSearch = isSearching(state);
const getModeSpecificProps = (
state: StateType
): LeftPanePropsType['modeSpecificProps'] => {
if (isComposing(state)) {
return {
mode: LeftPaneMode.Compose,
composeContacts: getComposeContacts(state),
regionCode: getRegionCode(state),
searchTerm: getComposerContactSearchTerm(state),
};
}
const lists = showSearch ? undefined : getLeftPaneLists(state);
const searchResults = showSearch ? getSearchResults(state) : undefined;
const selectedConversationId = getSelectedConversationId(state);
if (getShowArchived(state)) {
const { archivedConversations } = getLeftPaneLists(state);
return {
mode: LeftPaneMode.Archive,
archivedConversations,
};
}
if (isSearching(state)) {
return {
mode: LeftPaneMode.Search,
...getSearchResults(state),
};
}
return {
...lists,
searchResults,
selectedConversationId,
mode: LeftPaneMode.Inbox,
...getLeftPaneLists(state),
};
};
const mapStateToProps = (state: StateType) => {
return {
modeSpecificProps: getModeSpecificProps(state),
selectedConversationId: getSelectedConversationId(state),
selectedMessageId: getSelectedMessage(state)?.id,
showArchived: getShowArchived(state),
i18n: getIntl(state),
regionCode: getRegionCode(state),
renderExpiredBuildDialog,
renderMainHeader,
renderMessageSearchResult,

View file

@ -1,27 +1,30 @@
// Copyright 2019-2020 Signal Messenger, LLC
// Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { CSSProperties } from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { StateType } from '../reducer';
import { MessageSearchResult } from '../../components/MessageSearchResult';
import { MessageSearchResult } from '../../components/conversationList/MessageSearchResult';
import { getIntl } from '../selectors/user';
import { getMessageSearchResultSelector } from '../selectors/search';
type SmartProps = {
id: string;
style: CSSProperties;
};
function mapStateToProps(state: StateType, ourProps: SmartProps) {
const { id } = ourProps;
const { id, style } = ourProps;
const props = getMessageSearchResultSelector(state)(id);
return {
...props,
i18n: getIntl(state),
style,
};
}
const smart = connect(mapStateToProps, mapDispatchToProps);