Add "new conversation" composer for direct messages
This commit is contained in:
parent
84dc166b63
commit
06fb4fd0bc
61 changed files with 5960 additions and 3887 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue