2021-01-06 15:41:43 +00:00
|
|
|
// Copyright 2019-2021 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2019-03-20 17:42:28 +00:00
|
|
|
import memoizee from 'memoizee';
|
2021-02-23 20:34:28 +00:00
|
|
|
import { fromPairs, isNumber, isString } from 'lodash';
|
2019-01-14 21:49:58 +00:00
|
|
|
import { createSelector } from 'reselect';
|
|
|
|
|
|
|
|
import { StateType } from '../reducer';
|
|
|
|
import {
|
2021-03-03 20:09:58 +00:00
|
|
|
ComposerStep,
|
2019-01-14 21:49:58 +00:00
|
|
|
ConversationLookupType,
|
2019-05-31 22:42:01 +00:00
|
|
|
ConversationMessageType,
|
2019-01-14 21:49:58 +00:00
|
|
|
ConversationsStateType,
|
|
|
|
ConversationType,
|
2019-03-20 17:42:28 +00:00
|
|
|
MessageLookupType,
|
|
|
|
MessagesByConversationType,
|
|
|
|
MessageType,
|
2021-03-03 20:09:58 +00:00
|
|
|
OneTimeModalState,
|
2021-01-29 22:16:48 +00:00
|
|
|
PreJoinConversationType,
|
2019-01-14 21:49:58 +00:00
|
|
|
} from '../ducks/conversations';
|
2021-01-06 15:41:43 +00:00
|
|
|
import { getOwn } from '../../util/getOwn';
|
2021-03-03 20:09:58 +00:00
|
|
|
import { deconstructLookup } from '../../util/deconstructLookup';
|
2020-12-07 20:43:19 +00:00
|
|
|
import type { CallsByConversationType } from '../ducks/calling';
|
|
|
|
import { getCallsByConversation } from './calling';
|
2019-05-31 22:42:01 +00:00
|
|
|
import { getBubbleProps } from '../../shims/Whisper';
|
|
|
|
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
|
|
|
|
import { TimelineItemType } from '../../components/conversation/TimelineItem';
|
2021-02-12 21:58:14 +00:00
|
|
|
import { assert } from '../../util/assert';
|
2021-02-23 20:34:28 +00:00
|
|
|
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
2021-04-28 20:44:48 +00:00
|
|
|
import { filterAndSortConversationsByTitle } from '../../util/filterAndSortConversations';
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2019-11-21 19:16:06 +00:00
|
|
|
import {
|
|
|
|
getInteractionMode,
|
|
|
|
getIntl,
|
|
|
|
getRegionCode,
|
2020-03-05 21:14:58 +00:00
|
|
|
getUserConversationId,
|
2019-11-21 19:16:06 +00:00
|
|
|
getUserNumber,
|
|
|
|
} from './user';
|
2020-12-04 20:41:40 +00:00
|
|
|
import { getPinnedConversationIds } from './items';
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2021-01-06 15:41:43 +00:00
|
|
|
let placeholderContact: ConversationType;
|
|
|
|
export const getPlaceholderContact = (): ConversationType => {
|
|
|
|
if (placeholderContact) {
|
|
|
|
return placeholderContact;
|
|
|
|
}
|
|
|
|
|
|
|
|
placeholderContact = {
|
|
|
|
id: 'placeholder-contact',
|
|
|
|
type: 'direct',
|
|
|
|
title: window.i18n('unknownContact'),
|
|
|
|
};
|
|
|
|
return placeholderContact;
|
|
|
|
};
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
export const getConversations = (state: StateType): ConversationsStateType =>
|
|
|
|
state.conversations;
|
|
|
|
|
2021-01-29 22:16:48 +00:00
|
|
|
export const getPreJoinConversation = createSelector(
|
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType): PreJoinConversationType | undefined => {
|
|
|
|
return state.preJoinConversation;
|
|
|
|
}
|
|
|
|
);
|
2019-01-14 21:49:58 +00:00
|
|
|
export const getConversationLookup = createSelector(
|
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType): ConversationLookupType => {
|
|
|
|
return state.conversationLookup;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2021-01-06 15:41:43 +00:00
|
|
|
export const getConversationsByUuid = createSelector(
|
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType): ConversationLookupType => {
|
|
|
|
return state.conversationsByUuid;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getConversationsByE164 = createSelector(
|
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType): ConversationLookupType => {
|
|
|
|
return state.conversationsByE164;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getConversationsByGroupId = createSelector(
|
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType): ConversationLookupType => {
|
|
|
|
return state.conversationsByGroupId;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2021-04-21 16:31:12 +00:00
|
|
|
const getAllConversations = createSelector(
|
|
|
|
getConversationLookup,
|
|
|
|
(lookup): Array<ConversationType> => Object.values(lookup)
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getConversationsByTitleSelector = createSelector(
|
|
|
|
getAllConversations,
|
|
|
|
(conversations): ((title: string) => Array<ConversationType>) => (
|
|
|
|
title: string
|
|
|
|
) => conversations.filter(conversation => conversation.title === title)
|
|
|
|
);
|
|
|
|
|
2021-02-12 21:58:14 +00:00
|
|
|
export const getSelectedConversationId = createSelector(
|
2019-01-14 21:49:58 +00:00
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType): string | undefined => {
|
2021-02-12 21:58:14 +00:00
|
|
|
return state.selectedConversationId;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getSelectedConversation = createSelector(
|
|
|
|
getSelectedConversationId,
|
|
|
|
getConversationLookup,
|
|
|
|
(
|
|
|
|
selectedConversationId: string | undefined,
|
|
|
|
conversationLookup: ConversationLookupType
|
|
|
|
): undefined | ConversationType => {
|
|
|
|
if (!selectedConversationId) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const conversation = getOwn(conversationLookup, selectedConversationId);
|
|
|
|
assert(
|
|
|
|
conversation,
|
|
|
|
'getSelectedConversation: could not find selected conversation in lookup; returning undefined'
|
|
|
|
);
|
|
|
|
return conversation;
|
2019-01-14 21:49:58 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
type SelectedMessageType = {
|
|
|
|
id: string;
|
|
|
|
counter: number;
|
|
|
|
};
|
|
|
|
export const getSelectedMessage = createSelector(
|
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType): SelectedMessageType | undefined => {
|
|
|
|
if (!state.selectedMessage) {
|
2020-09-14 21:56:35 +00:00
|
|
|
return undefined;
|
2019-05-31 22:42:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: state.selectedMessage,
|
|
|
|
counter: state.selectedMessageCounter,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2019-03-12 00:20:16 +00:00
|
|
|
export const getShowArchived = createSelector(
|
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType): boolean => {
|
|
|
|
return Boolean(state.showArchived);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2021-02-23 20:34:28 +00:00
|
|
|
const getComposerState = createSelector(
|
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType) => state.composer
|
|
|
|
);
|
|
|
|
|
2021-03-03 20:09:58 +00:00
|
|
|
export const getComposerStep = createSelector(
|
2021-02-23 20:34:28 +00:00
|
|
|
getComposerState,
|
2021-03-03 20:09:58 +00:00
|
|
|
(composerState): undefined | ComposerStep => composerState?.step
|
|
|
|
);
|
|
|
|
|
|
|
|
export const hasGroupCreationError = createSelector(
|
|
|
|
getComposerState,
|
|
|
|
(composerState): boolean => {
|
|
|
|
if (composerState?.step === ComposerStep.SetGroupMetadata) {
|
|
|
|
return composerState.hasError;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const isCreatingGroup = createSelector(
|
|
|
|
getComposerState,
|
|
|
|
(composerState): boolean =>
|
|
|
|
composerState?.step === ComposerStep.SetGroupMetadata &&
|
|
|
|
composerState.isCreating
|
2021-02-23 20:34:28 +00:00
|
|
|
);
|
|
|
|
|
2019-03-20 17:42:28 +00:00
|
|
|
export const getMessages = createSelector(
|
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType): MessageLookupType => {
|
|
|
|
return state.messagesLookup;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
export const getMessagesByConversation = createSelector(
|
|
|
|
getConversations,
|
|
|
|
(state: ConversationsStateType): MessagesByConversationType => {
|
|
|
|
return state.messagesByConversation;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
const collator = new Intl.Collator();
|
|
|
|
|
2020-07-24 01:35:32 +00:00
|
|
|
// Note: we will probably want to put i18n and regionCode back when we are formatting
|
|
|
|
// phone numbers and contacts from scratch here again.
|
|
|
|
export const _getConversationComparator = () => {
|
2019-01-14 21:49:58 +00:00
|
|
|
return (left: ConversationType, right: ConversationType): number => {
|
|
|
|
const leftTimestamp = left.timestamp;
|
|
|
|
const rightTimestamp = right.timestamp;
|
|
|
|
if (leftTimestamp && !rightTimestamp) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if (rightTimestamp && !leftTimestamp) {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
if (leftTimestamp && rightTimestamp && leftTimestamp !== rightTimestamp) {
|
|
|
|
return rightTimestamp - leftTimestamp;
|
|
|
|
}
|
|
|
|
|
2020-03-10 00:43:09 +00:00
|
|
|
if (
|
|
|
|
typeof left.inboxPosition === 'number' &&
|
|
|
|
typeof right.inboxPosition === 'number'
|
|
|
|
) {
|
|
|
|
return right.inboxPosition > left.inboxPosition ? -1 : 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof left.inboxPosition === 'number' && right.inboxPosition == null) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof right.inboxPosition === 'number' && left.inboxPosition == null) {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2020-07-24 01:35:32 +00:00
|
|
|
return collator.compare(left.title, right.title);
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
export const getConversationComparator = createSelector(
|
|
|
|
getIntl,
|
|
|
|
getRegionCode,
|
|
|
|
_getConversationComparator
|
|
|
|
);
|
|
|
|
|
2019-03-12 00:20:16 +00:00
|
|
|
export const _getLeftPaneLists = (
|
2019-01-14 21:49:58 +00:00
|
|
|
lookup: ConversationLookupType,
|
|
|
|
comparator: (left: ConversationType, right: ConversationType) => number,
|
2020-12-04 20:41:40 +00:00
|
|
|
selectedConversation?: string,
|
|
|
|
pinnedConversationIds?: Array<string>
|
2019-03-12 00:20:16 +00:00
|
|
|
): {
|
|
|
|
conversations: Array<ConversationType>;
|
|
|
|
archivedConversations: Array<ConversationType>;
|
2020-09-29 22:07:03 +00:00
|
|
|
pinnedConversations: Array<ConversationType>;
|
2019-03-12 00:20:16 +00:00
|
|
|
} => {
|
|
|
|
const conversations: Array<ConversationType> = [];
|
|
|
|
const archivedConversations: Array<ConversationType> = [];
|
2020-09-29 22:07:03 +00:00
|
|
|
const pinnedConversations: Array<ConversationType> = [];
|
2019-03-12 00:20:16 +00:00
|
|
|
|
2019-06-19 22:58:54 +00:00
|
|
|
const values = Object.values(lookup);
|
|
|
|
const max = values.length;
|
2019-03-12 00:20:16 +00:00
|
|
|
for (let i = 0; i < max; i += 1) {
|
2019-06-19 22:58:54 +00:00
|
|
|
let conversation = values[i];
|
2020-09-14 21:56:35 +00:00
|
|
|
if (conversation.activeAt) {
|
|
|
|
if (selectedConversation === conversation.id) {
|
|
|
|
conversation = {
|
|
|
|
...conversation,
|
|
|
|
isSelected: true,
|
|
|
|
};
|
|
|
|
}
|
2019-03-12 00:20:16 +00:00
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
if (conversation.isArchived) {
|
|
|
|
archivedConversations.push(conversation);
|
2020-09-29 22:07:03 +00:00
|
|
|
} else if (conversation.isPinned) {
|
|
|
|
pinnedConversations.push(conversation);
|
2020-09-14 21:56:35 +00:00
|
|
|
} else {
|
|
|
|
conversations.push(conversation);
|
|
|
|
}
|
2019-03-12 00:20:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-19 22:58:54 +00:00
|
|
|
conversations.sort(comparator);
|
|
|
|
archivedConversations.sort(comparator);
|
2020-10-10 14:25:17 +00:00
|
|
|
|
|
|
|
pinnedConversations.sort(
|
|
|
|
(a, b) =>
|
2020-12-04 20:41:40 +00:00
|
|
|
(pinnedConversationIds || []).indexOf(a.id) -
|
|
|
|
(pinnedConversationIds || []).indexOf(b.id)
|
2020-10-10 14:25:17 +00:00
|
|
|
);
|
2019-06-19 22:58:54 +00:00
|
|
|
|
2020-09-29 22:07:03 +00:00
|
|
|
return { conversations, archivedConversations, pinnedConversations };
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
|
2019-03-12 00:20:16 +00:00
|
|
|
export const getLeftPaneLists = createSelector(
|
2019-01-14 21:49:58 +00:00
|
|
|
getConversationLookup,
|
|
|
|
getConversationComparator,
|
2021-02-12 21:58:14 +00:00
|
|
|
getSelectedConversationId,
|
2020-12-04 20:41:40 +00:00
|
|
|
getPinnedConversationIds,
|
2019-03-12 00:20:16 +00:00
|
|
|
_getLeftPaneLists
|
2019-01-14 21:49:58 +00:00
|
|
|
);
|
|
|
|
|
2021-03-03 20:09:58 +00:00
|
|
|
export const getMaximumGroupSizeModalState = createSelector(
|
|
|
|
getComposerState,
|
|
|
|
(composerState): OneTimeModalState => {
|
|
|
|
switch (composerState?.step) {
|
|
|
|
case ComposerStep.ChooseGroupMembers:
|
|
|
|
case ComposerStep.SetGroupMetadata:
|
|
|
|
return composerState.maximumGroupSizeModalState;
|
|
|
|
default:
|
|
|
|
assert(
|
|
|
|
false,
|
|
|
|
'Can\'t get the maximum group size modal state in this composer state; returning "never shown"'
|
|
|
|
);
|
|
|
|
return OneTimeModalState.NeverShown;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getRecommendedGroupSizeModalState = createSelector(
|
|
|
|
getComposerState,
|
|
|
|
(composerState): OneTimeModalState => {
|
|
|
|
switch (composerState?.step) {
|
|
|
|
case ComposerStep.ChooseGroupMembers:
|
|
|
|
case ComposerStep.SetGroupMetadata:
|
|
|
|
return composerState.recommendedGroupSizeModalState;
|
|
|
|
default:
|
|
|
|
assert(
|
|
|
|
false,
|
|
|
|
'Can\'t get the recommended group size modal state in this composer state; returning "never shown"'
|
|
|
|
);
|
|
|
|
return OneTimeModalState.NeverShown;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
export const getMe = createSelector(
|
2020-03-05 21:14:58 +00:00
|
|
|
[getConversationLookup, getUserConversationId],
|
|
|
|
(
|
|
|
|
lookup: ConversationLookupType,
|
|
|
|
ourConversationId: string
|
|
|
|
): ConversationType => {
|
|
|
|
return lookup[ourConversationId];
|
2019-01-14 21:49:58 +00:00
|
|
|
}
|
|
|
|
);
|
2021-02-23 20:34:28 +00:00
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
export const getComposerConversationSearchTerm = createSelector(
|
2021-02-23 20:34:28 +00:00
|
|
|
getComposerState,
|
|
|
|
(composer): string => {
|
|
|
|
if (!composer) {
|
2021-04-20 23:16:49 +00:00
|
|
|
assert(false, 'getComposerConversationSearchTerm: composer is not open');
|
2021-02-23 20:34:28 +00:00
|
|
|
return '';
|
|
|
|
}
|
2021-03-03 20:09:58 +00:00
|
|
|
if (composer.step === ComposerStep.SetGroupMetadata) {
|
|
|
|
assert(
|
|
|
|
false,
|
2021-04-20 23:16:49 +00:00
|
|
|
'getComposerConversationSearchTerm: composer does not have a search term'
|
2021-03-03 20:09:58 +00:00
|
|
|
);
|
|
|
|
return '';
|
|
|
|
}
|
2021-04-20 23:16:49 +00:00
|
|
|
return composer.searchTerm;
|
2021-02-23 20:34:28 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
function canComposeConversation(conversation: ConversationType): boolean {
|
|
|
|
return Boolean(
|
|
|
|
!conversation.isMe &&
|
|
|
|
!conversation.isBlocked &&
|
|
|
|
!isConversationUnregistered(conversation) &&
|
|
|
|
(isString(conversation.name) || conversation.profileSharing)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-27 22:35:35 +00:00
|
|
|
export const getAllComposableConversations = createSelector(
|
|
|
|
getConversationLookup,
|
|
|
|
(conversationLookup: ConversationLookupType): Array<ConversationType> =>
|
|
|
|
Object.values(conversationLookup).filter(
|
2021-04-28 20:44:48 +00:00
|
|
|
conversation =>
|
|
|
|
!conversation.isBlocked &&
|
|
|
|
!conversation.isGroupV1AndDisabled &&
|
|
|
|
!isConversationUnregistered(conversation) &&
|
|
|
|
// All conversation should have a title except in weird cases where
|
|
|
|
// they don't, in that case we don't want to show these for Forwarding.
|
|
|
|
conversation.title
|
2021-04-27 22:35:35 +00:00
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
const getContactsAndMe = createSelector(
|
|
|
|
getConversationLookup,
|
|
|
|
(conversationLookup: ConversationLookupType): Array<ConversationType> =>
|
|
|
|
Object.values(conversationLookup).filter(
|
|
|
|
contact =>
|
|
|
|
contact.type === 'direct' &&
|
|
|
|
!contact.isBlocked &&
|
|
|
|
!isConversationUnregistered(contact) &&
|
|
|
|
(isString(contact.name) || contact.profileSharing)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
2021-02-23 20:34:28 +00:00
|
|
|
/**
|
2021-03-11 21:29:31 +00:00
|
|
|
* This returns contacts for the composer and group members, which isn't just your primary
|
|
|
|
* system contacts. It may include false positives, which is better than missing contacts.
|
2021-02-23 20:34:28 +00:00
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
2021-04-20 23:16:49 +00:00
|
|
|
export const getComposableContacts = createSelector(
|
|
|
|
getConversationLookup,
|
|
|
|
(conversationLookup: ConversationLookupType): Array<ConversationType> =>
|
|
|
|
Object.values(conversationLookup).filter(
|
|
|
|
conversation =>
|
|
|
|
conversation.type === 'direct' && canComposeConversation(conversation)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getComposableGroups = createSelector(
|
2021-02-23 20:34:28 +00:00
|
|
|
getConversationLookup,
|
|
|
|
(conversationLookup: ConversationLookupType): Array<ConversationType> =>
|
|
|
|
Object.values(conversationLookup).filter(
|
2021-04-20 23:16:49 +00:00
|
|
|
conversation =>
|
|
|
|
conversation.type === 'group' && canComposeConversation(conversation)
|
2021-02-23 20:34:28 +00:00
|
|
|
)
|
|
|
|
);
|
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
const getNormalizedComposerConversationSearchTerm = createSelector(
|
|
|
|
getComposerConversationSearchTerm,
|
2021-02-23 20:34:28 +00:00
|
|
|
(searchTerm: string): string => searchTerm.trim()
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getComposeContacts = createSelector(
|
2021-04-20 23:16:49 +00:00
|
|
|
getNormalizedComposerConversationSearchTerm,
|
2021-04-27 22:35:35 +00:00
|
|
|
getContactsAndMe,
|
2021-02-23 20:34:28 +00:00
|
|
|
(
|
|
|
|
searchTerm: string,
|
2021-04-27 22:35:35 +00:00
|
|
|
contacts: Array<ConversationType>
|
2021-02-23 20:34:28 +00:00
|
|
|
): Array<ConversationType> => {
|
2021-04-28 20:44:48 +00:00
|
|
|
return filterAndSortConversationsByTitle(contacts, searchTerm);
|
2021-02-23 20:34:28 +00:00
|
|
|
}
|
|
|
|
);
|
2019-03-20 17:42:28 +00:00
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
export const getComposeGroups = createSelector(
|
|
|
|
getNormalizedComposerConversationSearchTerm,
|
|
|
|
getComposableGroups,
|
|
|
|
(
|
|
|
|
searchTerm: string,
|
|
|
|
groups: Array<ConversationType>
|
|
|
|
): Array<ConversationType> => {
|
2021-04-28 20:44:48 +00:00
|
|
|
return filterAndSortConversationsByTitle(groups, searchTerm);
|
2021-04-20 23:16:49 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2021-03-11 21:29:31 +00:00
|
|
|
export const getCandidateContactsForNewGroup = createSelector(
|
2021-04-20 23:16:49 +00:00
|
|
|
getComposableContacts,
|
|
|
|
getNormalizedComposerConversationSearchTerm,
|
2021-04-28 20:44:48 +00:00
|
|
|
filterAndSortConversationsByTitle
|
2021-03-03 20:09:58 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
export const getCantAddContactForModal = createSelector(
|
|
|
|
getConversationLookup,
|
|
|
|
getComposerState,
|
|
|
|
(conversationLookup, composerState): undefined | ConversationType => {
|
|
|
|
if (composerState?.step !== ComposerStep.ChooseGroupMembers) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const conversationId = composerState.cantAddContactIdForModal;
|
|
|
|
if (!conversationId) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = getOwn(conversationLookup, conversationId);
|
|
|
|
assert(
|
|
|
|
result,
|
|
|
|
'getCantAddContactForModal: failed to look up conversation by ID; returning undefined'
|
|
|
|
);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const getGroupCreationComposerState = createSelector(
|
|
|
|
getComposerState,
|
|
|
|
(
|
|
|
|
composerState
|
|
|
|
): {
|
|
|
|
groupName: string;
|
|
|
|
groupAvatar: undefined | ArrayBuffer;
|
|
|
|
selectedConversationIds: Array<string>;
|
|
|
|
} => {
|
|
|
|
switch (composerState?.step) {
|
|
|
|
case ComposerStep.ChooseGroupMembers:
|
|
|
|
case ComposerStep.SetGroupMetadata:
|
|
|
|
return composerState;
|
|
|
|
default:
|
|
|
|
assert(
|
|
|
|
false,
|
|
|
|
'getSetGroupMetadataComposerState: expected step to be SetGroupMetadata'
|
|
|
|
);
|
|
|
|
return {
|
|
|
|
groupName: '',
|
|
|
|
groupAvatar: undefined,
|
|
|
|
selectedConversationIds: [],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getComposeGroupAvatar = createSelector(
|
|
|
|
getGroupCreationComposerState,
|
|
|
|
(composerState): undefined | ArrayBuffer => composerState.groupAvatar
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getComposeGroupName = createSelector(
|
|
|
|
getGroupCreationComposerState,
|
|
|
|
(composerState): string => composerState.groupName
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getComposeSelectedContacts = createSelector(
|
|
|
|
getConversationLookup,
|
|
|
|
getGroupCreationComposerState,
|
|
|
|
(conversationLookup, composerState): Array<ConversationType> =>
|
|
|
|
deconstructLookup(conversationLookup, composerState.selectedConversationIds)
|
|
|
|
);
|
|
|
|
|
2019-03-20 17:42:28 +00:00
|
|
|
// This is where we will put Conversation selector logic, replicating what
|
2019-05-31 22:42:01 +00:00
|
|
|
// is currently in models/conversation.getProps()
|
|
|
|
// What needs to happen to pull that selector logic here?
|
|
|
|
// 1) contactTypingTimers - that UI-only state needs to be moved to redux
|
|
|
|
// 2) all of the message selectors need to be reselect-based; today those
|
|
|
|
// Backbone-based prop-generation functions expect to get Conversation information
|
|
|
|
// directly via ConversationController
|
2019-03-20 17:42:28 +00:00
|
|
|
export function _conversationSelector(
|
2021-01-06 15:41:43 +00:00
|
|
|
conversation?: ConversationType
|
2019-03-20 17:42:28 +00:00
|
|
|
// regionCode: string,
|
|
|
|
// userNumber: string
|
|
|
|
): ConversationType {
|
2021-01-06 15:41:43 +00:00
|
|
|
if (conversation) {
|
|
|
|
return conversation;
|
|
|
|
}
|
|
|
|
|
|
|
|
return getPlaceholderContact();
|
2019-03-20 17:42:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// A little optimization to reset our selector cache when high-level application data
|
|
|
|
// changes: regionCode and userNumber.
|
|
|
|
type CachedConversationSelectorType = (
|
2021-01-06 15:41:43 +00:00
|
|
|
conversation?: ConversationType
|
2019-03-20 17:42:28 +00:00
|
|
|
) => ConversationType;
|
|
|
|
export const getCachedSelectorForConversation = createSelector(
|
|
|
|
getRegionCode,
|
|
|
|
getUserNumber,
|
|
|
|
(): CachedConversationSelectorType => {
|
2019-05-31 22:42:01 +00:00
|
|
|
// Note: memoizee will check all parameters provided, and only run our selector
|
|
|
|
// if any of them have changed.
|
2019-08-09 00:46:49 +00:00
|
|
|
return memoizee(_conversationSelector, { max: 2000 });
|
2019-03-20 17:42:28 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2021-01-06 15:41:43 +00:00
|
|
|
export type GetConversationByIdType = (id?: string) => ConversationType;
|
2019-03-20 17:42:28 +00:00
|
|
|
export const getConversationSelector = createSelector(
|
|
|
|
getCachedSelectorForConversation,
|
|
|
|
getConversationLookup,
|
2021-01-06 15:41:43 +00:00
|
|
|
getConversationsByUuid,
|
|
|
|
getConversationsByE164,
|
|
|
|
getConversationsByGroupId,
|
2019-03-20 17:42:28 +00:00
|
|
|
(
|
|
|
|
selector: CachedConversationSelectorType,
|
2021-01-06 15:41:43 +00:00
|
|
|
byId: ConversationLookupType,
|
|
|
|
byUuid: ConversationLookupType,
|
|
|
|
byE164: ConversationLookupType,
|
|
|
|
byGroupId: ConversationLookupType
|
2019-03-20 17:42:28 +00:00
|
|
|
): GetConversationByIdType => {
|
2021-01-06 15:41:43 +00:00
|
|
|
return (id?: string) => {
|
|
|
|
if (!id) {
|
|
|
|
window.log.warn(
|
|
|
|
`getConversationSelector: Called with a falsey id ${id}`
|
|
|
|
);
|
|
|
|
// This will return a placeholder contact
|
|
|
|
return selector(undefined);
|
|
|
|
}
|
|
|
|
|
|
|
|
const onE164 = getOwn(byE164, id);
|
|
|
|
if (onE164) {
|
|
|
|
return selector(onE164);
|
|
|
|
}
|
2021-02-26 15:08:59 +00:00
|
|
|
const onUuid = getOwn(byUuid, id.toLowerCase ? id.toLowerCase() : id);
|
2021-01-06 15:41:43 +00:00
|
|
|
if (onUuid) {
|
|
|
|
return selector(onUuid);
|
|
|
|
}
|
|
|
|
const onGroupId = getOwn(byGroupId, id);
|
|
|
|
if (onGroupId) {
|
|
|
|
return selector(onGroupId);
|
|
|
|
}
|
|
|
|
const onId = getOwn(byId, id);
|
|
|
|
if (onId) {
|
|
|
|
return selector(onId);
|
2019-03-20 17:42:28 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 15:41:43 +00:00
|
|
|
window.log.warn(
|
|
|
|
`getConversationSelector: No conversation found for id ${id}`
|
|
|
|
);
|
|
|
|
// This will return a placeholder contact
|
|
|
|
return selector(undefined);
|
2019-03-20 17:42:28 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2021-04-29 18:32:38 +00:00
|
|
|
export const getConversationByIdSelector = createSelector(
|
|
|
|
getConversationLookup,
|
|
|
|
conversationLookup => (id: string): undefined | ConversationType =>
|
|
|
|
getOwn(conversationLookup, id)
|
|
|
|
);
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
// For now we use a shim, as selector logic is still happening in the Backbone Model.
|
|
|
|
// What needs to happen to pull that selector logic here?
|
|
|
|
// 1) translate ~500 lines of selector logic into TypeScript
|
|
|
|
// 2) other places still rely on that prop-gen code - need to put these under Roots:
|
|
|
|
// - quote compose
|
|
|
|
// - message details
|
2019-03-20 17:42:28 +00:00
|
|
|
export function _messageSelector(
|
2019-05-31 22:42:01 +00:00
|
|
|
message: MessageType,
|
2020-09-14 21:56:35 +00:00
|
|
|
_ourNumber: string,
|
|
|
|
_regionCode: string,
|
2019-11-21 19:16:06 +00:00
|
|
|
interactionMode: 'mouse' | 'keyboard',
|
2021-03-26 18:03:15 +00:00
|
|
|
_getConversationById: GetConversationByIdType,
|
2020-12-07 20:43:19 +00:00
|
|
|
_callsByConversation: CallsByConversationType,
|
2019-05-31 22:42:01 +00:00
|
|
|
selectedMessageId?: string,
|
|
|
|
selectedMessageCounter?: number
|
|
|
|
): TimelineItemType {
|
|
|
|
// Note: We don't use all of those parameters here, but the shim we call does.
|
|
|
|
// We want to call this function again if any of those parameters change.
|
|
|
|
const props = getBubbleProps(message);
|
|
|
|
|
|
|
|
if (selectedMessageId === message.id) {
|
|
|
|
return {
|
|
|
|
...props,
|
|
|
|
data: {
|
|
|
|
...props.data,
|
2019-11-21 19:16:06 +00:00
|
|
|
interactionMode,
|
2019-05-31 22:42:01 +00:00
|
|
|
isSelected: true,
|
|
|
|
isSelectedCounter: selectedMessageCounter,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-11-21 19:16:06 +00:00
|
|
|
return {
|
|
|
|
...props,
|
|
|
|
data: {
|
|
|
|
...props.data,
|
|
|
|
interactionMode,
|
|
|
|
},
|
|
|
|
};
|
2019-03-20 17:42:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// A little optimization to reset our selector cache whenever high-level application data
|
|
|
|
// changes: regionCode and userNumber.
|
2019-05-31 22:42:01 +00:00
|
|
|
type CachedMessageSelectorType = (
|
|
|
|
message: MessageType,
|
|
|
|
ourNumber: string,
|
|
|
|
regionCode: string,
|
2019-11-21 19:16:06 +00:00
|
|
|
interactionMode: 'mouse' | 'keyboard',
|
2021-03-26 18:03:15 +00:00
|
|
|
getConversationById: GetConversationByIdType,
|
2020-12-07 20:43:19 +00:00
|
|
|
callsByConversation: CallsByConversationType,
|
2019-05-31 22:42:01 +00:00
|
|
|
selectedMessageId?: string,
|
|
|
|
selectedMessageCounter?: number
|
|
|
|
) => TimelineItemType;
|
2019-03-20 17:42:28 +00:00
|
|
|
export const getCachedSelectorForMessage = createSelector(
|
|
|
|
getRegionCode,
|
|
|
|
getUserNumber,
|
|
|
|
(): CachedMessageSelectorType => {
|
2019-05-31 22:42:01 +00:00
|
|
|
// Note: memoizee will check all parameters provided, and only run our selector
|
|
|
|
// if any of them have changed.
|
2019-08-09 00:46:49 +00:00
|
|
|
return memoizee(_messageSelector, { max: 2000 });
|
2019-03-20 17:42:28 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
type GetMessageByIdType = (id: string) => TimelineItemType | undefined;
|
2019-03-20 17:42:28 +00:00
|
|
|
export const getMessageSelector = createSelector(
|
|
|
|
getCachedSelectorForMessage,
|
|
|
|
getMessages,
|
2019-05-31 22:42:01 +00:00
|
|
|
getSelectedMessage,
|
|
|
|
getConversationSelector,
|
|
|
|
getRegionCode,
|
|
|
|
getUserNumber,
|
2019-11-21 19:16:06 +00:00
|
|
|
getInteractionMode,
|
2020-12-07 20:43:19 +00:00
|
|
|
getCallsByConversation,
|
2019-03-20 17:42:28 +00:00
|
|
|
(
|
2019-05-31 22:42:01 +00:00
|
|
|
messageSelector: CachedMessageSelectorType,
|
|
|
|
messageLookup: MessageLookupType,
|
|
|
|
selectedMessage: SelectedMessageType | undefined,
|
|
|
|
conversationSelector: GetConversationByIdType,
|
|
|
|
regionCode: string,
|
2019-11-21 19:16:06 +00:00
|
|
|
ourNumber: string,
|
2020-12-07 20:43:19 +00:00
|
|
|
interactionMode: 'keyboard' | 'mouse',
|
|
|
|
callsByConversation: CallsByConversationType
|
2019-03-20 17:42:28 +00:00
|
|
|
): GetMessageByIdType => {
|
|
|
|
return (id: string) => {
|
2019-05-31 22:42:01 +00:00
|
|
|
const message = messageLookup[id];
|
2019-03-20 17:42:28 +00:00
|
|
|
if (!message) {
|
2020-09-14 21:56:35 +00:00
|
|
|
return undefined;
|
2019-03-20 17:42:28 +00:00
|
|
|
}
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
return messageSelector(
|
|
|
|
message,
|
|
|
|
ourNumber,
|
|
|
|
regionCode,
|
2019-11-21 19:16:06 +00:00
|
|
|
interactionMode,
|
2021-03-26 18:03:15 +00:00
|
|
|
conversationSelector,
|
2020-12-07 20:43:19 +00:00
|
|
|
callsByConversation,
|
2019-05-31 22:42:01 +00:00
|
|
|
selectedMessage ? selectedMessage.id : undefined,
|
|
|
|
selectedMessage ? selectedMessage.counter : undefined
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export function _conversationMessagesSelector(
|
|
|
|
conversation: ConversationMessageType
|
|
|
|
): TimelinePropsType {
|
|
|
|
const {
|
|
|
|
heightChangeMessageIds,
|
|
|
|
isLoadingMessages,
|
2019-09-03 20:06:17 +00:00
|
|
|
isNearBottom,
|
2019-05-31 22:42:01 +00:00
|
|
|
loadCountdownStart,
|
|
|
|
messageIds,
|
|
|
|
metrics,
|
|
|
|
resetCounter,
|
|
|
|
scrollToMessageId,
|
|
|
|
scrollToMessageCounter,
|
|
|
|
} = conversation;
|
|
|
|
|
|
|
|
const firstId = messageIds[0];
|
|
|
|
const lastId =
|
|
|
|
messageIds.length === 0 ? undefined : messageIds[messageIds.length - 1];
|
|
|
|
|
|
|
|
const { oldestUnread } = metrics;
|
|
|
|
|
|
|
|
const haveNewest = !metrics.newest || !lastId || lastId === metrics.newest.id;
|
|
|
|
const haveOldest =
|
|
|
|
!metrics.oldest || !firstId || firstId === metrics.oldest.id;
|
|
|
|
|
|
|
|
const items = messageIds;
|
2019-08-23 19:56:49 +00:00
|
|
|
|
|
|
|
const messageHeightChangeLookup =
|
2019-05-31 22:42:01 +00:00
|
|
|
heightChangeMessageIds && heightChangeMessageIds.length
|
2019-08-23 19:56:49 +00:00
|
|
|
? fromPairs(heightChangeMessageIds.map(id => [id, true]))
|
|
|
|
: null;
|
|
|
|
const messageHeightChangeIndex = messageHeightChangeLookup
|
|
|
|
? messageIds.findIndex(id => messageHeightChangeLookup[id])
|
|
|
|
: undefined;
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
const oldestUnreadIndex = oldestUnread
|
|
|
|
? messageIds.findIndex(id => id === oldestUnread.id)
|
|
|
|
: undefined;
|
|
|
|
const scrollToIndex = scrollToMessageId
|
|
|
|
? messageIds.findIndex(id => id === scrollToMessageId)
|
|
|
|
: undefined;
|
|
|
|
const { totalUnread } = metrics;
|
|
|
|
|
|
|
|
return {
|
|
|
|
haveNewest,
|
|
|
|
haveOldest,
|
|
|
|
isLoadingMessages,
|
|
|
|
loadCountdownStart,
|
|
|
|
items,
|
2019-09-03 20:06:17 +00:00
|
|
|
isNearBottom,
|
2019-08-23 19:56:49 +00:00
|
|
|
messageHeightChangeIndex:
|
|
|
|
isNumber(messageHeightChangeIndex) && messageHeightChangeIndex >= 0
|
|
|
|
? messageHeightChangeIndex
|
|
|
|
: undefined,
|
2019-05-31 22:42:01 +00:00
|
|
|
oldestUnreadIndex:
|
2019-08-15 14:59:56 +00:00
|
|
|
isNumber(oldestUnreadIndex) && oldestUnreadIndex >= 0
|
2019-05-31 22:42:01 +00:00
|
|
|
? oldestUnreadIndex
|
|
|
|
: undefined,
|
|
|
|
resetCounter,
|
|
|
|
scrollToIndex:
|
2019-08-23 19:56:49 +00:00
|
|
|
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
2019-05-31 22:42:01 +00:00
|
|
|
scrollToIndexCounter: scrollToMessageCounter,
|
|
|
|
totalUnread,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
type CachedConversationMessagesSelectorType = (
|
|
|
|
conversation: ConversationMessageType
|
|
|
|
) => TimelinePropsType;
|
|
|
|
export const getCachedSelectorForConversationMessages = createSelector(
|
|
|
|
getRegionCode,
|
|
|
|
getUserNumber,
|
|
|
|
(): CachedConversationMessagesSelectorType => {
|
|
|
|
// Note: memoizee will check all parameters provided, and only run our selector
|
|
|
|
// if any of them have changed.
|
|
|
|
return memoizee(_conversationMessagesSelector, { max: 50 });
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const getConversationMessagesSelector = createSelector(
|
|
|
|
getCachedSelectorForConversationMessages,
|
|
|
|
getMessagesByConversation,
|
|
|
|
(
|
|
|
|
conversationMessagesSelector: CachedConversationMessagesSelectorType,
|
|
|
|
messagesByConversation: MessagesByConversationType
|
|
|
|
) => {
|
|
|
|
return (id: string): TimelinePropsType | undefined => {
|
|
|
|
const conversation = messagesByConversation[id];
|
|
|
|
if (!conversation) {
|
2020-09-14 21:56:35 +00:00
|
|
|
return undefined;
|
2019-05-31 22:42:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return conversationMessagesSelector(conversation);
|
2019-03-20 17:42:28 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
2021-03-03 20:09:58 +00:00
|
|
|
|
|
|
|
export const getInvitedContactsForNewlyCreatedGroup = createSelector(
|
|
|
|
getConversationLookup,
|
|
|
|
getConversations,
|
|
|
|
(
|
|
|
|
conversationLookup,
|
|
|
|
{ invitedConversationIdsForNewlyCreatedGroup = [] }
|
|
|
|
): Array<ConversationType> =>
|
|
|
|
deconstructLookup(
|
|
|
|
conversationLookup,
|
|
|
|
invitedConversationIdsForNewlyCreatedGroup
|
|
|
|
)
|
|
|
|
);
|