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
|
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
/* eslint-disable camelcase */
|
2020-12-09 23:21:34 +00:00
|
|
|
import { ThunkAction } from 'redux-thunk';
|
2019-05-31 22:42:01 +00:00
|
|
|
import {
|
|
|
|
difference,
|
|
|
|
fromPairs,
|
|
|
|
intersection,
|
|
|
|
omit,
|
|
|
|
orderBy,
|
|
|
|
pick,
|
|
|
|
uniq,
|
|
|
|
values,
|
|
|
|
without,
|
|
|
|
} from 'lodash';
|
2020-12-04 20:41:40 +00:00
|
|
|
|
2020-12-09 23:21:34 +00:00
|
|
|
import { StateType as RootStateType } from '../reducer';
|
|
|
|
import { calling } from '../../services/calling';
|
2020-12-04 20:41:40 +00:00
|
|
|
import { getOwn } from '../../util/getOwn';
|
2019-01-14 21:49:58 +00:00
|
|
|
import { trigger } from '../../shims/events';
|
|
|
|
import { NoopActionType } from './noop';
|
2019-09-04 21:11:30 +00:00
|
|
|
import { AttachmentType } from '../../types/Attachment';
|
2020-08-13 20:53:45 +00:00
|
|
|
import { ColorType } from '../../types/Colors';
|
2020-11-03 01:19:52 +00:00
|
|
|
import { BodyRangeType } from '../../types/Util';
|
2020-12-07 20:43:19 +00:00
|
|
|
import { CallMode, CallHistoryDetailsFromDiskType } from '../../types/Calling';
|
2021-01-29 21:19:24 +00:00
|
|
|
import {
|
|
|
|
GroupV2PendingMembership,
|
|
|
|
GroupV2RequestingMembership,
|
|
|
|
} from '../../components/conversation/conversation-details/PendingInvites';
|
|
|
|
import { GroupV2Membership } from '../../components/conversation/conversation-details/ConversationDetailsMembershipList';
|
|
|
|
import { MediaItemType } from '../../components/LightboxGallery';
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
|
|
// State
|
|
|
|
|
2020-02-06 19:52:05 +00:00
|
|
|
export type DBConversationType = {
|
|
|
|
id: string;
|
|
|
|
activeAt?: number;
|
|
|
|
lastMessage: string;
|
|
|
|
type: string;
|
|
|
|
};
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
export type LastMessageStatus =
|
|
|
|
| 'error'
|
|
|
|
| 'partial-sent'
|
|
|
|
| 'sending'
|
|
|
|
| 'sent'
|
|
|
|
| 'delivered'
|
|
|
|
| 'read';
|
|
|
|
|
|
|
|
export type ConversationTypeType = 'direct' | 'group';
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
export type ConversationType = {
|
|
|
|
id: string;
|
2020-06-26 00:08:58 +00:00
|
|
|
uuid?: string;
|
2020-07-24 01:35:32 +00:00
|
|
|
e164?: string;
|
2019-01-14 21:49:58 +00:00
|
|
|
name?: string;
|
2020-07-29 23:20:05 +00:00
|
|
|
firstName?: string;
|
2020-05-27 21:37:06 +00:00
|
|
|
profileName?: string;
|
2021-01-26 01:01:19 +00:00
|
|
|
about?: string;
|
2020-05-27 21:37:06 +00:00
|
|
|
avatarPath?: string;
|
2020-11-11 17:36:05 +00:00
|
|
|
areWeAdmin?: boolean;
|
2020-10-26 14:39:45 +00:00
|
|
|
areWePending?: boolean;
|
2021-01-29 22:16:48 +00:00
|
|
|
areWePendingApproval?: boolean;
|
2020-10-30 17:52:21 +00:00
|
|
|
canChangeTimer?: boolean;
|
2021-01-29 21:19:24 +00:00
|
|
|
canEditGroupInfo?: boolean;
|
2020-05-27 21:37:06 +00:00
|
|
|
color?: ColorType;
|
2020-10-30 17:52:21 +00:00
|
|
|
isAccepted?: boolean;
|
2020-05-27 21:37:06 +00:00
|
|
|
isArchived?: boolean;
|
|
|
|
isBlocked?: boolean;
|
2020-12-01 16:42:35 +00:00
|
|
|
isGroupV1AndDisabled?: boolean;
|
2020-09-29 22:07:03 +00:00
|
|
|
isPinned?: boolean;
|
2020-12-08 19:37:04 +00:00
|
|
|
isUntrusted?: boolean;
|
2020-06-26 00:08:58 +00:00
|
|
|
isVerified?: boolean;
|
2019-01-14 21:49:58 +00:00
|
|
|
activeAt?: number;
|
2020-07-24 01:35:32 +00:00
|
|
|
timestamp?: number;
|
|
|
|
inboxPosition?: number;
|
2020-10-30 17:52:21 +00:00
|
|
|
left?: boolean;
|
2019-01-14 21:49:58 +00:00
|
|
|
lastMessage?: {
|
2020-09-24 20:57:54 +00:00
|
|
|
status: LastMessageStatus;
|
2019-01-14 21:49:58 +00:00
|
|
|
text: string;
|
2020-10-28 21:54:33 +00:00
|
|
|
deletedForEveryone?: boolean;
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
2020-11-20 17:30:45 +00:00
|
|
|
markedUnread?: boolean;
|
2020-07-24 01:35:32 +00:00
|
|
|
phoneNumber?: string;
|
2020-05-27 21:37:06 +00:00
|
|
|
membersCount?: number;
|
2021-01-29 21:19:24 +00:00
|
|
|
accessControlAddFromInviteLink?: number;
|
|
|
|
accessControlAttributes?: number;
|
|
|
|
accessControlMembers?: number;
|
2020-10-30 17:52:21 +00:00
|
|
|
expireTimer?: number;
|
2021-01-29 21:19:24 +00:00
|
|
|
// This is used by the ConversationDetails set of components, it includes the
|
|
|
|
// membersV2 data and also has some extra metadata attached to the object
|
|
|
|
memberships?: Array<GroupV2Membership>;
|
|
|
|
pendingMemberships?: Array<GroupV2PendingMembership>;
|
|
|
|
pendingApprovalMemberships?: Array<GroupV2RequestingMembership>;
|
2020-08-27 19:45:08 +00:00
|
|
|
muteExpiresAt?: number;
|
2020-09-24 20:57:54 +00:00
|
|
|
type: ConversationTypeType;
|
2020-07-29 23:20:05 +00:00
|
|
|
isMe?: boolean;
|
2020-11-20 17:30:45 +00:00
|
|
|
lastUpdated?: number;
|
2021-01-29 21:19:24 +00:00
|
|
|
// This is used by the CompositionInput for @mentions
|
|
|
|
sortedGroupMembers?: Array<ConversationType>;
|
2020-06-26 00:08:58 +00:00
|
|
|
title: string;
|
2020-07-29 23:20:05 +00:00
|
|
|
unreadCount?: number;
|
|
|
|
isSelected?: boolean;
|
2019-05-31 22:42:01 +00:00
|
|
|
typingContact?: {
|
|
|
|
avatarPath?: string;
|
2020-09-24 20:57:54 +00:00
|
|
|
color?: ColorType;
|
2019-05-31 22:42:01 +00:00
|
|
|
name?: string;
|
2020-09-24 20:57:54 +00:00
|
|
|
phoneNumber?: string;
|
2019-05-31 22:42:01 +00:00
|
|
|
profileName?: string;
|
2020-09-24 20:57:54 +00:00
|
|
|
} | null;
|
2021-01-29 21:19:24 +00:00
|
|
|
recentMediaItems?: Array<MediaItemType>;
|
2019-08-07 00:40:25 +00:00
|
|
|
|
|
|
|
shouldShowDraft?: boolean;
|
2020-09-24 20:57:54 +00:00
|
|
|
draftText?: string | null;
|
2020-11-03 01:19:52 +00:00
|
|
|
draftBodyRanges?: Array<BodyRangeType>;
|
2019-08-07 00:40:25 +00:00
|
|
|
draftPreview?: string;
|
2020-05-27 21:37:06 +00:00
|
|
|
|
2020-10-28 21:54:33 +00:00
|
|
|
sharedGroupNames?: Array<string>;
|
2020-10-16 18:31:57 +00:00
|
|
|
groupVersion?: 1 | 2;
|
2020-11-13 19:57:55 +00:00
|
|
|
groupId?: string;
|
2021-01-29 21:19:24 +00:00
|
|
|
groupLink?: string;
|
2020-10-16 18:31:57 +00:00
|
|
|
isMissingMandatoryProfileSharing?: boolean;
|
2020-05-27 21:37:06 +00:00
|
|
|
messageRequestsEnabled?: boolean;
|
|
|
|
acceptedMessageRequest?: boolean;
|
2020-11-13 19:57:55 +00:00
|
|
|
secretParams?: string;
|
|
|
|
publicParams?: string;
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
export type ConversationLookupType = {
|
|
|
|
[key: string]: ConversationType;
|
|
|
|
};
|
2019-03-20 17:42:28 +00:00
|
|
|
export type MessageType = {
|
|
|
|
id: string;
|
2019-05-31 22:42:01 +00:00
|
|
|
conversationId: string;
|
2021-01-06 15:41:43 +00:00
|
|
|
source?: string;
|
|
|
|
sourceUuid?: string;
|
2020-03-10 00:43:09 +00:00
|
|
|
type:
|
|
|
|
| 'incoming'
|
|
|
|
| 'outgoing'
|
|
|
|
| 'group'
|
|
|
|
| 'keychange'
|
|
|
|
| 'verified-change'
|
2020-06-04 18:16:19 +00:00
|
|
|
| 'message-history-unsynced'
|
|
|
|
| 'call-history';
|
2019-05-31 22:42:01 +00:00
|
|
|
quote?: { author: string };
|
|
|
|
received_at: number;
|
2020-10-23 19:20:21 +00:00
|
|
|
sent_at?: number;
|
2019-05-31 22:42:01 +00:00
|
|
|
hasSignalAccount?: boolean;
|
2019-10-17 17:19:41 +00:00
|
|
|
bodyPending?: boolean;
|
2019-08-20 19:34:52 +00:00
|
|
|
attachments: Array<AttachmentType>;
|
2019-08-22 22:04:14 +00:00
|
|
|
sticker: {
|
|
|
|
data?: {
|
2020-11-12 21:22:40 +00:00
|
|
|
pending?: boolean;
|
|
|
|
blurHash?: string;
|
2019-08-22 22:04:14 +00:00
|
|
|
};
|
|
|
|
};
|
2020-02-03 20:02:49 +00:00
|
|
|
unread: boolean;
|
|
|
|
reactions?: Array<{
|
|
|
|
emoji: string;
|
|
|
|
timestamp: number;
|
|
|
|
from: {
|
|
|
|
id: string;
|
|
|
|
color?: string;
|
|
|
|
avatarPath?: string;
|
|
|
|
name?: string;
|
|
|
|
profileName?: string;
|
|
|
|
isMe?: boolean;
|
|
|
|
phoneNumber?: string;
|
|
|
|
};
|
|
|
|
}>;
|
2020-04-29 21:24:12 +00:00
|
|
|
deletedForEveryone?: boolean;
|
2019-05-31 22:42:01 +00:00
|
|
|
|
2020-03-25 22:48:10 +00:00
|
|
|
errors?: Array<Error>;
|
2020-09-14 21:56:35 +00:00
|
|
|
group_update?: unknown;
|
2020-12-07 20:43:19 +00:00
|
|
|
callHistoryDetails?: CallHistoryDetailsFromDiskType;
|
2020-03-25 22:48:10 +00:00
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
// No need to go beyond this; unused at this stage, since this goes into
|
|
|
|
// a reducer still in plain JavaScript and comes out well-formed
|
2019-03-20 17:42:28 +00:00
|
|
|
};
|
2019-05-31 22:42:01 +00:00
|
|
|
|
|
|
|
type MessagePointerType = {
|
|
|
|
id: string;
|
|
|
|
received_at: number;
|
2021-01-13 16:32:18 +00:00
|
|
|
sent_at?: number;
|
2019-05-31 22:42:01 +00:00
|
|
|
};
|
|
|
|
type MessageMetricsType = {
|
|
|
|
newest?: MessagePointerType;
|
|
|
|
oldest?: MessagePointerType;
|
|
|
|
oldestUnread?: MessagePointerType;
|
|
|
|
totalUnread: number;
|
|
|
|
};
|
|
|
|
|
2019-03-20 17:42:28 +00:00
|
|
|
export type MessageLookupType = {
|
|
|
|
[key: string]: MessageType;
|
|
|
|
};
|
|
|
|
export type ConversationMessageType = {
|
2019-05-31 22:42:01 +00:00
|
|
|
heightChangeMessageIds: Array<string>;
|
|
|
|
isLoadingMessages: boolean;
|
|
|
|
isNearBottom?: boolean;
|
|
|
|
loadCountdownStart?: number;
|
|
|
|
messageIds: Array<string>;
|
|
|
|
metrics: MessageMetricsType;
|
|
|
|
resetCounter: number;
|
|
|
|
scrollToMessageId?: string;
|
|
|
|
scrollToMessageCounter: number;
|
2019-03-20 17:42:28 +00:00
|
|
|
};
|
2019-05-31 22:42:01 +00:00
|
|
|
|
2019-03-20 17:42:28 +00:00
|
|
|
export type MessagesByConversationType = {
|
2021-01-06 15:41:43 +00:00
|
|
|
[key: string]: ConversationMessageType | undefined;
|
2019-03-20 17:42:28 +00:00
|
|
|
};
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2021-01-29 22:16:48 +00:00
|
|
|
export type PreJoinConversationType = {
|
|
|
|
avatar?: {
|
|
|
|
loading?: boolean;
|
|
|
|
url?: string;
|
|
|
|
};
|
|
|
|
memberCount: number;
|
|
|
|
title: string;
|
|
|
|
approvalRequired: boolean;
|
|
|
|
};
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
export type ConversationsStateType = {
|
2021-01-29 22:16:48 +00:00
|
|
|
preJoinConversation?: PreJoinConversationType;
|
2019-01-14 21:49:58 +00:00
|
|
|
conversationLookup: ConversationLookupType;
|
2021-01-06 15:41:43 +00:00
|
|
|
conversationsByE164: ConversationLookupType;
|
|
|
|
conversationsByUuid: ConversationLookupType;
|
|
|
|
conversationsByGroupId: ConversationLookupType;
|
2019-01-14 21:49:58 +00:00
|
|
|
selectedConversation?: string;
|
2019-05-31 22:42:01 +00:00
|
|
|
selectedMessage?: string;
|
|
|
|
selectedMessageCounter: number;
|
2021-01-29 21:19:24 +00:00
|
|
|
selectedConversationTitle?: string;
|
2020-10-30 17:52:21 +00:00
|
|
|
selectedConversationPanelDepth: number;
|
2019-03-12 00:20:16 +00:00
|
|
|
showArchived: boolean;
|
2019-03-20 17:42:28 +00:00
|
|
|
|
|
|
|
// Note: it's very important that both of these locations are always kept up to date
|
|
|
|
messagesLookup: MessageLookupType;
|
|
|
|
messagesByConversation: MessagesByConversationType;
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
// Helpers
|
|
|
|
|
|
|
|
export const getConversationCallMode = (
|
|
|
|
conversation: ConversationType
|
|
|
|
): CallMode => {
|
|
|
|
if (
|
|
|
|
conversation.left ||
|
|
|
|
conversation.isBlocked ||
|
|
|
|
conversation.isMe ||
|
|
|
|
!conversation.acceptedMessageRequest
|
|
|
|
) {
|
|
|
|
return CallMode.None;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (conversation.type === 'direct') {
|
|
|
|
return CallMode.Direct;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (conversation.type === 'group' && conversation.groupVersion === 2) {
|
|
|
|
return CallMode.Group;
|
|
|
|
}
|
|
|
|
|
|
|
|
return CallMode.None;
|
|
|
|
};
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
// Actions
|
|
|
|
|
2021-01-29 22:16:48 +00:00
|
|
|
type SetPreJoinConversationActionType = {
|
|
|
|
type: 'SET_PRE_JOIN_CONVERSATION';
|
|
|
|
payload: {
|
|
|
|
data: PreJoinConversationType | undefined;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
type ConversationAddedActionType = {
|
|
|
|
type: 'CONVERSATION_ADDED';
|
|
|
|
payload: {
|
|
|
|
id: string;
|
|
|
|
data: ConversationType;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
type ConversationChangedActionType = {
|
|
|
|
type: 'CONVERSATION_CHANGED';
|
|
|
|
payload: {
|
|
|
|
id: string;
|
|
|
|
data: ConversationType;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
type ConversationRemovedActionType = {
|
|
|
|
type: 'CONVERSATION_REMOVED';
|
|
|
|
payload: {
|
|
|
|
id: string;
|
|
|
|
};
|
|
|
|
};
|
2019-11-07 21:36:16 +00:00
|
|
|
export type ConversationUnloadedActionType = {
|
2019-05-31 22:42:01 +00:00
|
|
|
type: 'CONVERSATION_UNLOADED';
|
|
|
|
payload: {
|
|
|
|
id: string;
|
|
|
|
};
|
|
|
|
};
|
2019-01-14 21:49:58 +00:00
|
|
|
export type RemoveAllConversationsActionType = {
|
|
|
|
type: 'CONVERSATIONS_REMOVE_ALL';
|
|
|
|
payload: null;
|
|
|
|
};
|
2019-11-07 21:36:16 +00:00
|
|
|
export type MessageSelectedActionType = {
|
|
|
|
type: 'MESSAGE_SELECTED';
|
|
|
|
payload: {
|
|
|
|
messageId: string;
|
|
|
|
conversationId: string;
|
|
|
|
};
|
|
|
|
};
|
2019-05-31 22:42:01 +00:00
|
|
|
export type MessageChangedActionType = {
|
|
|
|
type: 'MESSAGE_CHANGED';
|
2019-01-14 21:49:58 +00:00
|
|
|
payload: {
|
|
|
|
id: string;
|
|
|
|
conversationId: string;
|
2019-05-31 22:42:01 +00:00
|
|
|
data: MessageType;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
export type MessageDeletedActionType = {
|
|
|
|
type: 'MESSAGE_DELETED';
|
|
|
|
payload: {
|
|
|
|
id: string;
|
|
|
|
conversationId: string;
|
|
|
|
};
|
|
|
|
};
|
2020-12-07 20:43:19 +00:00
|
|
|
type MessageSizeChangedActionType = {
|
|
|
|
type: 'MESSAGE_SIZE_CHANGED';
|
|
|
|
payload: {
|
|
|
|
id: string;
|
|
|
|
conversationId: string;
|
|
|
|
};
|
|
|
|
};
|
2019-05-31 22:42:01 +00:00
|
|
|
export type MessagesAddedActionType = {
|
|
|
|
type: 'MESSAGES_ADDED';
|
|
|
|
payload: {
|
|
|
|
conversationId: string;
|
|
|
|
messages: Array<MessageType>;
|
|
|
|
isNewMessage: boolean;
|
2019-09-19 22:16:46 +00:00
|
|
|
isActive: boolean;
|
2019-05-31 22:42:01 +00:00
|
|
|
};
|
|
|
|
};
|
2020-12-04 20:41:40 +00:00
|
|
|
|
|
|
|
export type RepairNewestMessageActionType = {
|
|
|
|
type: 'REPAIR_NEWEST_MESSAGE';
|
|
|
|
payload: {
|
|
|
|
conversationId: string;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
export type RepairOldestMessageActionType = {
|
|
|
|
type: 'REPAIR_OLDEST_MESSAGE';
|
|
|
|
payload: {
|
|
|
|
conversationId: string;
|
|
|
|
};
|
|
|
|
};
|
2019-05-31 22:42:01 +00:00
|
|
|
export type MessagesResetActionType = {
|
|
|
|
type: 'MESSAGES_RESET';
|
|
|
|
payload: {
|
|
|
|
conversationId: string;
|
|
|
|
messages: Array<MessageType>;
|
|
|
|
metrics: MessageMetricsType;
|
|
|
|
scrollToMessageId?: string;
|
2020-07-06 17:06:44 +00:00
|
|
|
// The set of provided messages should be trusted, even if it conflicts with metrics,
|
|
|
|
// because we weren't looking for a specific time window of messages with our query.
|
|
|
|
unboundedFetch: boolean;
|
2019-05-31 22:42:01 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
export type SetMessagesLoadingActionType = {
|
|
|
|
type: 'SET_MESSAGES_LOADING';
|
|
|
|
payload: {
|
|
|
|
conversationId: string;
|
|
|
|
isLoadingMessages: boolean;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
export type SetLoadCountdownStartActionType = {
|
|
|
|
type: 'SET_LOAD_COUNTDOWN_START';
|
|
|
|
payload: {
|
|
|
|
conversationId: string;
|
|
|
|
loadCountdownStart?: number;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
export type SetIsNearBottomActionType = {
|
|
|
|
type: 'SET_NEAR_BOTTOM';
|
|
|
|
payload: {
|
|
|
|
conversationId: string;
|
|
|
|
isNearBottom: boolean;
|
|
|
|
};
|
|
|
|
};
|
2021-01-29 21:19:24 +00:00
|
|
|
export type SetConversationHeaderTitleActionType = {
|
|
|
|
type: 'SET_CONVERSATION_HEADER_TITLE';
|
|
|
|
payload: { title?: string };
|
|
|
|
};
|
2020-10-30 17:52:21 +00:00
|
|
|
export type SetSelectedConversationPanelDepthActionType = {
|
|
|
|
type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH';
|
|
|
|
payload: { panelDepth: number };
|
|
|
|
};
|
2019-05-31 22:42:01 +00:00
|
|
|
export type ScrollToMessageActionType = {
|
|
|
|
type: 'SCROLL_TO_MESSAGE';
|
|
|
|
payload: {
|
|
|
|
conversationId: string;
|
|
|
|
messageId: string;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
export type ClearChangedMessagesActionType = {
|
|
|
|
type: 'CLEAR_CHANGED_MESSAGES';
|
|
|
|
payload: {
|
|
|
|
conversationId: string;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
export type ClearSelectedMessageActionType = {
|
|
|
|
type: 'CLEAR_SELECTED_MESSAGE';
|
|
|
|
payload: null;
|
|
|
|
};
|
|
|
|
export type ClearUnreadMetricsActionType = {
|
|
|
|
type: 'CLEAR_UNREAD_METRICS';
|
|
|
|
payload: {
|
|
|
|
conversationId: string;
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
export type SelectedConversationChangedActionType = {
|
|
|
|
type: 'SELECTED_CONVERSATION_CHANGED';
|
|
|
|
payload: {
|
|
|
|
id: string;
|
|
|
|
messageId?: string;
|
|
|
|
};
|
|
|
|
};
|
2019-03-12 00:20:16 +00:00
|
|
|
type ShowInboxActionType = {
|
|
|
|
type: 'SHOW_INBOX';
|
|
|
|
payload: null;
|
|
|
|
};
|
2019-11-07 21:36:16 +00:00
|
|
|
export type ShowArchivedConversationsActionType = {
|
2019-03-12 00:20:16 +00:00
|
|
|
type: 'SHOW_ARCHIVED_CONVERSATIONS';
|
|
|
|
payload: null;
|
|
|
|
};
|
2021-01-29 21:19:24 +00:00
|
|
|
type SetRecentMediaItemsActionType = {
|
|
|
|
type: 'SET_RECENT_MEDIA_ITEMS';
|
|
|
|
payload: {
|
|
|
|
id: string;
|
|
|
|
recentMediaItems: Array<MediaItemType>;
|
|
|
|
};
|
|
|
|
};
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
|
|
export type ConversationActionType =
|
2021-01-29 22:16:48 +00:00
|
|
|
| ClearChangedMessagesActionType
|
|
|
|
| ClearSelectedMessageActionType
|
|
|
|
| ClearUnreadMetricsActionType
|
2019-01-14 21:49:58 +00:00
|
|
|
| ConversationAddedActionType
|
|
|
|
| ConversationChangedActionType
|
|
|
|
| ConversationRemovedActionType
|
2019-05-31 22:42:01 +00:00
|
|
|
| ConversationUnloadedActionType
|
|
|
|
| MessageChangedActionType
|
|
|
|
| MessageDeletedActionType
|
|
|
|
| MessagesAddedActionType
|
2021-01-29 22:16:48 +00:00
|
|
|
| MessageSelectedActionType
|
|
|
|
| MessageSizeChangedActionType
|
|
|
|
| MessagesResetActionType
|
|
|
|
| RemoveAllConversationsActionType
|
2020-12-04 20:41:40 +00:00
|
|
|
| RepairNewestMessageActionType
|
|
|
|
| RepairOldestMessageActionType
|
2019-05-31 22:42:01 +00:00
|
|
|
| ScrollToMessageActionType
|
2019-03-12 00:20:16 +00:00
|
|
|
| SelectedConversationChangedActionType
|
2021-01-29 22:16:48 +00:00
|
|
|
| SetConversationHeaderTitleActionType
|
|
|
|
| SetIsNearBottomActionType
|
|
|
|
| SetLoadCountdownStartActionType
|
|
|
|
| SetMessagesLoadingActionType
|
|
|
|
| SetPreJoinConversationActionType
|
2021-01-29 21:19:24 +00:00
|
|
|
| SetRecentMediaItemsActionType
|
2021-01-29 22:16:48 +00:00
|
|
|
| SetSelectedConversationPanelDepthActionType
|
|
|
|
| ShowArchivedConversationsActionType
|
|
|
|
| ShowInboxActionType;
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
|
|
// Action Creators
|
|
|
|
|
|
|
|
export const actions = {
|
2021-01-29 21:19:24 +00:00
|
|
|
clearChangedMessages,
|
|
|
|
clearSelectedMessage,
|
|
|
|
clearUnreadMetrics,
|
2019-01-14 21:49:58 +00:00
|
|
|
conversationAdded,
|
|
|
|
conversationChanged,
|
|
|
|
conversationRemoved,
|
2019-05-31 22:42:01 +00:00
|
|
|
conversationUnloaded,
|
|
|
|
messageChanged,
|
2021-01-29 21:19:24 +00:00
|
|
|
messageDeleted,
|
2019-05-31 22:42:01 +00:00
|
|
|
messagesAdded,
|
2021-01-29 22:16:48 +00:00
|
|
|
messageSizeChanged,
|
2019-05-31 22:42:01 +00:00
|
|
|
messagesReset,
|
2019-01-14 21:49:58 +00:00
|
|
|
openConversationExternal,
|
2021-01-29 21:19:24 +00:00
|
|
|
openConversationInternal,
|
|
|
|
removeAllConversations,
|
2020-12-04 20:41:40 +00:00
|
|
|
repairNewestMessage,
|
|
|
|
repairOldestMessage,
|
2021-01-29 21:19:24 +00:00
|
|
|
scrollToMessage,
|
|
|
|
selectMessage,
|
|
|
|
setIsNearBottom,
|
|
|
|
setLoadCountdownStart,
|
|
|
|
setMessagesLoading,
|
2021-01-29 22:16:48 +00:00
|
|
|
setPreJoinConversation,
|
2021-01-29 21:19:24 +00:00
|
|
|
setRecentMediaItems,
|
|
|
|
setSelectedConversationHeaderTitle,
|
|
|
|
setSelectedConversationPanelDepth,
|
|
|
|
showArchivedConversations,
|
|
|
|
showInbox,
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
|
2021-01-29 22:16:48 +00:00
|
|
|
function setPreJoinConversation(
|
|
|
|
data: PreJoinConversationType | undefined
|
|
|
|
): SetPreJoinConversationActionType {
|
|
|
|
return {
|
|
|
|
type: 'SET_PRE_JOIN_CONVERSATION',
|
|
|
|
payload: {
|
|
|
|
data,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2019-01-14 21:49:58 +00:00
|
|
|
function conversationAdded(
|
|
|
|
id: string,
|
|
|
|
data: ConversationType
|
|
|
|
): ConversationAddedActionType {
|
|
|
|
return {
|
|
|
|
type: 'CONVERSATION_ADDED',
|
|
|
|
payload: {
|
|
|
|
id,
|
|
|
|
data,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
function conversationChanged(
|
|
|
|
id: string,
|
|
|
|
data: ConversationType
|
2020-12-09 23:21:34 +00:00
|
|
|
): ThunkAction<void, RootStateType, unknown, ConversationChangedActionType> {
|
|
|
|
return dispatch => {
|
|
|
|
calling.groupMembersChanged(id);
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: 'CONVERSATION_CHANGED',
|
|
|
|
payload: {
|
|
|
|
id,
|
|
|
|
data,
|
|
|
|
},
|
|
|
|
});
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
function conversationRemoved(id: string): ConversationRemovedActionType {
|
|
|
|
return {
|
|
|
|
type: 'CONVERSATION_REMOVED',
|
|
|
|
payload: {
|
|
|
|
id,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2019-05-31 22:42:01 +00:00
|
|
|
function conversationUnloaded(id: string): ConversationUnloadedActionType {
|
|
|
|
return {
|
|
|
|
type: 'CONVERSATION_UNLOADED',
|
|
|
|
payload: {
|
|
|
|
id,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2019-01-14 21:49:58 +00:00
|
|
|
function removeAllConversations(): RemoveAllConversationsActionType {
|
|
|
|
return {
|
|
|
|
type: 'CONVERSATIONS_REMOVE_ALL',
|
|
|
|
payload: null,
|
|
|
|
};
|
|
|
|
}
|
2019-03-12 00:20:16 +00:00
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
function selectMessage(
|
|
|
|
messageId: string,
|
|
|
|
conversationId: string
|
|
|
|
): MessageSelectedActionType {
|
2019-11-07 21:36:16 +00:00
|
|
|
return {
|
|
|
|
type: 'MESSAGE_SELECTED',
|
|
|
|
payload: {
|
|
|
|
messageId,
|
|
|
|
conversationId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
function messageChanged(
|
|
|
|
id: string,
|
|
|
|
conversationId: string,
|
|
|
|
data: MessageType
|
|
|
|
): MessageChangedActionType {
|
|
|
|
return {
|
|
|
|
type: 'MESSAGE_CHANGED',
|
|
|
|
payload: {
|
|
|
|
id,
|
|
|
|
conversationId,
|
|
|
|
data,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
function messageDeleted(
|
2019-01-14 21:49:58 +00:00
|
|
|
id: string,
|
|
|
|
conversationId: string
|
2019-05-31 22:42:01 +00:00
|
|
|
): MessageDeletedActionType {
|
2019-01-14 21:49:58 +00:00
|
|
|
return {
|
2019-05-31 22:42:01 +00:00
|
|
|
type: 'MESSAGE_DELETED',
|
2019-01-14 21:49:58 +00:00
|
|
|
payload: {
|
|
|
|
id,
|
|
|
|
conversationId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2020-12-07 20:43:19 +00:00
|
|
|
function messageSizeChanged(
|
|
|
|
id: string,
|
|
|
|
conversationId: string
|
|
|
|
): MessageSizeChangedActionType {
|
|
|
|
return {
|
|
|
|
type: 'MESSAGE_SIZE_CHANGED',
|
|
|
|
payload: {
|
|
|
|
id,
|
|
|
|
conversationId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2019-05-31 22:42:01 +00:00
|
|
|
function messagesAdded(
|
|
|
|
conversationId: string,
|
|
|
|
messages: Array<MessageType>,
|
|
|
|
isNewMessage: boolean,
|
2019-09-19 22:16:46 +00:00
|
|
|
isActive: boolean
|
2019-05-31 22:42:01 +00:00
|
|
|
): MessagesAddedActionType {
|
|
|
|
return {
|
|
|
|
type: 'MESSAGES_ADDED',
|
|
|
|
payload: {
|
|
|
|
conversationId,
|
|
|
|
messages,
|
|
|
|
isNewMessage,
|
2019-09-19 22:16:46 +00:00
|
|
|
isActive,
|
2019-05-31 22:42:01 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2020-12-04 20:41:40 +00:00
|
|
|
|
|
|
|
function repairNewestMessage(
|
|
|
|
conversationId: string
|
|
|
|
): RepairNewestMessageActionType {
|
|
|
|
return {
|
|
|
|
type: 'REPAIR_NEWEST_MESSAGE',
|
|
|
|
payload: {
|
|
|
|
conversationId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
function repairOldestMessage(
|
|
|
|
conversationId: string
|
|
|
|
): RepairOldestMessageActionType {
|
|
|
|
return {
|
|
|
|
type: 'REPAIR_OLDEST_MESSAGE',
|
|
|
|
payload: {
|
|
|
|
conversationId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
function messagesReset(
|
|
|
|
conversationId: string,
|
|
|
|
messages: Array<MessageType>,
|
|
|
|
metrics: MessageMetricsType,
|
2020-07-06 17:06:44 +00:00
|
|
|
scrollToMessageId?: string,
|
|
|
|
unboundedFetch?: boolean
|
2019-05-31 22:42:01 +00:00
|
|
|
): MessagesResetActionType {
|
|
|
|
return {
|
|
|
|
type: 'MESSAGES_RESET',
|
|
|
|
payload: {
|
2020-07-06 17:06:44 +00:00
|
|
|
unboundedFetch: Boolean(unboundedFetch),
|
2019-05-31 22:42:01 +00:00
|
|
|
conversationId,
|
|
|
|
messages,
|
|
|
|
metrics,
|
|
|
|
scrollToMessageId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
function setMessagesLoading(
|
|
|
|
conversationId: string,
|
|
|
|
isLoadingMessages: boolean
|
|
|
|
): SetMessagesLoadingActionType {
|
|
|
|
return {
|
|
|
|
type: 'SET_MESSAGES_LOADING',
|
|
|
|
payload: {
|
|
|
|
conversationId,
|
|
|
|
isLoadingMessages,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
function setLoadCountdownStart(
|
|
|
|
conversationId: string,
|
|
|
|
loadCountdownStart?: number
|
|
|
|
): SetLoadCountdownStartActionType {
|
|
|
|
return {
|
|
|
|
type: 'SET_LOAD_COUNTDOWN_START',
|
|
|
|
payload: {
|
|
|
|
conversationId,
|
|
|
|
loadCountdownStart,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
function setIsNearBottom(
|
|
|
|
conversationId: string,
|
|
|
|
isNearBottom: boolean
|
|
|
|
): SetIsNearBottomActionType {
|
|
|
|
return {
|
|
|
|
type: 'SET_NEAR_BOTTOM',
|
|
|
|
payload: {
|
|
|
|
conversationId,
|
|
|
|
isNearBottom,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2021-01-29 21:19:24 +00:00
|
|
|
function setSelectedConversationHeaderTitle(
|
|
|
|
title?: string
|
|
|
|
): SetConversationHeaderTitleActionType {
|
|
|
|
return {
|
|
|
|
type: 'SET_CONVERSATION_HEADER_TITLE',
|
|
|
|
payload: { title },
|
|
|
|
};
|
|
|
|
}
|
2020-10-30 17:52:21 +00:00
|
|
|
function setSelectedConversationPanelDepth(
|
|
|
|
panelDepth: number
|
|
|
|
): SetSelectedConversationPanelDepthActionType {
|
|
|
|
return {
|
|
|
|
type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH',
|
|
|
|
payload: { panelDepth },
|
|
|
|
};
|
|
|
|
}
|
2021-01-29 21:19:24 +00:00
|
|
|
function setRecentMediaItems(
|
|
|
|
id: string,
|
|
|
|
recentMediaItems: Array<MediaItemType>
|
|
|
|
): SetRecentMediaItemsActionType {
|
|
|
|
return {
|
|
|
|
type: 'SET_RECENT_MEDIA_ITEMS',
|
|
|
|
payload: { id, recentMediaItems },
|
|
|
|
};
|
|
|
|
}
|
2019-05-31 22:42:01 +00:00
|
|
|
function clearChangedMessages(
|
|
|
|
conversationId: string
|
|
|
|
): ClearChangedMessagesActionType {
|
|
|
|
return {
|
|
|
|
type: 'CLEAR_CHANGED_MESSAGES',
|
|
|
|
payload: {
|
|
|
|
conversationId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
function clearSelectedMessage(): ClearSelectedMessageActionType {
|
|
|
|
return {
|
|
|
|
type: 'CLEAR_SELECTED_MESSAGE',
|
|
|
|
payload: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
function clearUnreadMetrics(
|
|
|
|
conversationId: string
|
|
|
|
): ClearUnreadMetricsActionType {
|
|
|
|
return {
|
|
|
|
type: 'CLEAR_UNREAD_METRICS',
|
|
|
|
payload: {
|
|
|
|
conversationId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function scrollToMessage(
|
|
|
|
conversationId: string,
|
|
|
|
messageId: string
|
|
|
|
): ScrollToMessageActionType {
|
|
|
|
return {
|
|
|
|
type: 'SCROLL_TO_MESSAGE',
|
|
|
|
payload: {
|
|
|
|
conversationId,
|
|
|
|
messageId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
|
|
// Note: we need two actions here to simplify. Operations outside of the left pane can
|
2019-05-31 22:42:01 +00:00
|
|
|
// 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.
|
2019-01-14 21:49:58 +00:00
|
|
|
function openConversationInternal(
|
|
|
|
id: string,
|
|
|
|
messageId?: string
|
|
|
|
): NoopActionType {
|
|
|
|
trigger('showConversation', id, messageId);
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: 'NOOP',
|
|
|
|
payload: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
function openConversationExternal(
|
|
|
|
id: string,
|
|
|
|
messageId?: string
|
|
|
|
): SelectedConversationChangedActionType {
|
|
|
|
return {
|
|
|
|
type: 'SELECTED_CONVERSATION_CHANGED',
|
|
|
|
payload: {
|
|
|
|
id,
|
|
|
|
messageId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
function showInbox(): ShowInboxActionType {
|
2019-03-12 00:20:16 +00:00
|
|
|
return {
|
|
|
|
type: 'SHOW_INBOX',
|
|
|
|
payload: null,
|
|
|
|
};
|
|
|
|
}
|
2020-09-14 21:56:35 +00:00
|
|
|
function showArchivedConversations(): ShowArchivedConversationsActionType {
|
2019-03-12 00:20:16 +00:00
|
|
|
return {
|
|
|
|
type: 'SHOW_ARCHIVED_CONVERSATIONS',
|
|
|
|
payload: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
// Reducer
|
|
|
|
|
2020-12-07 20:43:19 +00:00
|
|
|
export function getEmptyState(): ConversationsStateType {
|
2019-01-14 21:49:58 +00:00
|
|
|
return {
|
|
|
|
conversationLookup: {},
|
2021-01-06 15:41:43 +00:00
|
|
|
conversationsByE164: {},
|
|
|
|
conversationsByUuid: {},
|
|
|
|
conversationsByGroupId: {},
|
2019-03-20 17:42:28 +00:00
|
|
|
messagesByConversation: {},
|
2019-05-31 22:42:01 +00:00
|
|
|
messagesLookup: {},
|
|
|
|
selectedMessageCounter: 0,
|
|
|
|
showArchived: false,
|
2021-01-29 21:19:24 +00:00
|
|
|
selectedConversationTitle: '',
|
2020-10-30 17:52:21 +00:00
|
|
|
selectedConversationPanelDepth: 0,
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
function hasMessageHeightChanged(
|
|
|
|
message: MessageType,
|
|
|
|
previous: MessageType
|
2020-09-14 21:56:35 +00:00
|
|
|
): boolean {
|
2019-08-22 22:04:14 +00:00
|
|
|
const messageAttachments = message.attachments || [];
|
|
|
|
const previousAttachments = previous.attachments || [];
|
|
|
|
|
2020-03-25 22:48:10 +00:00
|
|
|
const errorStatusChanged =
|
|
|
|
(!message.errors && previous.errors) ||
|
|
|
|
(message.errors && !previous.errors) ||
|
|
|
|
(message.errors &&
|
|
|
|
previous.errors &&
|
|
|
|
message.errors.length !== previous.errors.length);
|
|
|
|
if (errorStatusChanged) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-03-26 21:47:35 +00:00
|
|
|
const groupUpdateChanged = message.group_update !== previous.group_update;
|
|
|
|
if (groupUpdateChanged) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-08-22 22:04:14 +00:00
|
|
|
const stickerPendingChanged =
|
|
|
|
message.sticker &&
|
|
|
|
message.sticker.data &&
|
|
|
|
previous.sticker &&
|
|
|
|
previous.sticker.data &&
|
2020-11-12 21:22:40 +00:00
|
|
|
!previous.sticker.data.blurHash &&
|
2019-08-22 22:04:14 +00:00
|
|
|
previous.sticker.data.pending !== message.sticker.data.pending;
|
|
|
|
if (stickerPendingChanged) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-10-17 17:19:41 +00:00
|
|
|
const longMessageAttachmentLoaded =
|
|
|
|
previous.bodyPending && !message.bodyPending;
|
|
|
|
if (longMessageAttachmentLoaded) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-09-04 21:11:30 +00:00
|
|
|
const firstAttachmentNoLongerPending =
|
2019-08-22 22:04:14 +00:00
|
|
|
previousAttachments[0] &&
|
|
|
|
previousAttachments[0].pending &&
|
|
|
|
messageAttachments[0] &&
|
|
|
|
!messageAttachments[0].pending;
|
2019-09-04 21:11:30 +00:00
|
|
|
if (firstAttachmentNoLongerPending) {
|
2019-08-22 22:04:14 +00:00
|
|
|
return true;
|
|
|
|
}
|
2019-08-20 19:34:52 +00:00
|
|
|
|
|
|
|
const signalAccountChanged =
|
2019-05-31 22:42:01 +00:00
|
|
|
Boolean(message.hasSignalAccount || previous.hasSignalAccount) &&
|
2019-08-20 19:34:52 +00:00
|
|
|
message.hasSignalAccount !== previous.hasSignalAccount;
|
2019-08-22 22:04:14 +00:00
|
|
|
if (signalAccountChanged) {
|
|
|
|
return true;
|
|
|
|
}
|
2019-08-20 19:34:52 +00:00
|
|
|
|
2020-02-03 20:02:49 +00:00
|
|
|
const currentReactions = message.reactions || [];
|
|
|
|
const lastReactions = previous.reactions || [];
|
|
|
|
const reactionsChanged =
|
|
|
|
(currentReactions.length === 0) !== (lastReactions.length === 0);
|
|
|
|
if (reactionsChanged) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-04-29 21:24:12 +00:00
|
|
|
const isDeletedForEveryone = message.deletedForEveryone;
|
|
|
|
const wasDeletedForEveryone = previous.deletedForEveryone;
|
|
|
|
if (isDeletedForEveryone !== wasDeletedForEveryone) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-08-22 22:04:14 +00:00
|
|
|
return false;
|
2019-05-31 22:42:01 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 15:41:43 +00:00
|
|
|
export function updateConversationLookups(
|
|
|
|
added: ConversationType | undefined,
|
|
|
|
removed: ConversationType | undefined,
|
|
|
|
state: ConversationsStateType
|
|
|
|
): Pick<
|
|
|
|
ConversationsStateType,
|
|
|
|
'conversationsByE164' | 'conversationsByUuid' | 'conversationsByGroupId'
|
|
|
|
> {
|
|
|
|
const result = {
|
|
|
|
conversationsByE164: state.conversationsByE164,
|
|
|
|
conversationsByUuid: state.conversationsByUuid,
|
|
|
|
conversationsByGroupId: state.conversationsByGroupId,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (removed && removed.e164) {
|
|
|
|
result.conversationsByE164 = omit(result.conversationsByE164, removed.e164);
|
|
|
|
}
|
|
|
|
if (removed && removed.uuid) {
|
|
|
|
result.conversationsByUuid = omit(result.conversationsByUuid, removed.uuid);
|
|
|
|
}
|
|
|
|
if (removed && removed.groupId) {
|
|
|
|
result.conversationsByGroupId = omit(
|
|
|
|
result.conversationsByGroupId,
|
|
|
|
removed.groupId
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (added && added.e164) {
|
|
|
|
result.conversationsByE164 = {
|
|
|
|
...result.conversationsByE164,
|
|
|
|
[added.e164]: added,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (added && added.uuid) {
|
|
|
|
result.conversationsByUuid = {
|
|
|
|
...result.conversationsByUuid,
|
|
|
|
[added.uuid]: added,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (added && added.groupId) {
|
|
|
|
result.conversationsByGroupId = {
|
|
|
|
...result.conversationsByGroupId,
|
|
|
|
[added.groupId]: added,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
export function reducer(
|
2020-12-14 19:47:21 +00:00
|
|
|
state: Readonly<ConversationsStateType> = getEmptyState(),
|
|
|
|
action: Readonly<ConversationActionType>
|
2019-01-14 21:49:58 +00:00
|
|
|
): ConversationsStateType {
|
2021-01-29 22:16:48 +00:00
|
|
|
if (action.type === 'SET_PRE_JOIN_CONVERSATION') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { data } = payload;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
preJoinConversation: data,
|
|
|
|
};
|
|
|
|
}
|
2019-01-14 21:49:58 +00:00
|
|
|
if (action.type === 'CONVERSATION_ADDED') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { id, data } = payload;
|
|
|
|
const { conversationLookup } = state;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
conversationLookup: {
|
|
|
|
...conversationLookup,
|
|
|
|
[id]: data,
|
|
|
|
},
|
2021-01-06 15:41:43 +00:00
|
|
|
...updateConversationLookups(data, undefined, state),
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'CONVERSATION_CHANGED') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { id, data } = payload;
|
|
|
|
const { conversationLookup } = state;
|
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
let { showArchived, selectedConversation } = state;
|
2019-03-12 00:20:16 +00:00
|
|
|
|
|
|
|
const existing = conversationLookup[id];
|
2019-01-14 21:49:58 +00:00
|
|
|
// In the change case we only modify the lookup if we already had that conversation
|
2019-03-12 00:20:16 +00:00
|
|
|
if (!existing) {
|
2019-01-14 21:49:58 +00:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2019-03-12 00:20:16 +00:00
|
|
|
if (selectedConversation === id) {
|
|
|
|
// Archived -> Inbox: we go back to the normal inbox view
|
|
|
|
if (existing.isArchived && !data.isArchived) {
|
|
|
|
showArchived = false;
|
|
|
|
}
|
|
|
|
// Inbox -> Archived: no conversation is selected
|
|
|
|
// Note: With today's stacked converastions architecture, this can result in weird
|
|
|
|
// behavior - no selected conversation in the left pane, but a conversation show
|
|
|
|
// in the right pane.
|
|
|
|
if (!existing.isArchived && data.isArchived) {
|
|
|
|
selectedConversation = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
return {
|
|
|
|
...state,
|
2019-03-12 00:20:16 +00:00
|
|
|
selectedConversation,
|
|
|
|
showArchived,
|
2019-01-14 21:49:58 +00:00
|
|
|
conversationLookup: {
|
|
|
|
...conversationLookup,
|
|
|
|
[id]: data,
|
|
|
|
},
|
2021-01-06 15:41:43 +00:00
|
|
|
...updateConversationLookups(data, existing, state),
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'CONVERSATION_REMOVED') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { id } = payload;
|
|
|
|
const { conversationLookup } = state;
|
2021-01-06 15:41:43 +00:00
|
|
|
const existing = getOwn(conversationLookup, id);
|
|
|
|
|
|
|
|
// No need to make a change if we didn't have a record of this conversation!
|
|
|
|
if (!existing) {
|
|
|
|
return state;
|
|
|
|
}
|
2019-01-14 21:49:58 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
conversationLookup: omit(conversationLookup, [id]),
|
2021-01-06 15:41:43 +00:00
|
|
|
...updateConversationLookups(undefined, existing, state),
|
2019-01-14 21:49:58 +00:00
|
|
|
};
|
|
|
|
}
|
2019-05-31 22:42:01 +00:00
|
|
|
if (action.type === 'CONVERSATION_UNLOADED') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { id } = payload;
|
|
|
|
const existingConversation = state.messagesByConversation[id];
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { messageIds } = existingConversation;
|
2019-11-07 21:36:16 +00:00
|
|
|
const selectedConversation =
|
|
|
|
state.selectedConversation !== id
|
|
|
|
? state.selectedConversation
|
|
|
|
: undefined;
|
2019-05-31 22:42:01 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
2019-11-07 21:36:16 +00:00
|
|
|
selectedConversation,
|
2020-10-30 17:52:21 +00:00
|
|
|
selectedConversationPanelDepth: 0,
|
2019-05-31 22:42:01 +00:00
|
|
|
messagesLookup: omit(state.messagesLookup, messageIds),
|
|
|
|
messagesByConversation: omit(state.messagesByConversation, [id]),
|
|
|
|
};
|
|
|
|
}
|
2019-01-14 21:49:58 +00:00
|
|
|
if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
|
|
|
|
return getEmptyState();
|
|
|
|
}
|
2020-10-30 17:52:21 +00:00
|
|
|
if (action.type === 'SET_SELECTED_CONVERSATION_PANEL_DEPTH') {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
selectedConversationPanelDepth: action.payload.panelDepth,
|
|
|
|
};
|
|
|
|
}
|
2019-11-07 21:36:16 +00:00
|
|
|
if (action.type === 'MESSAGE_SELECTED') {
|
|
|
|
const { messageId, conversationId } = action.payload;
|
|
|
|
|
|
|
|
if (state.selectedConversation !== conversationId) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
selectedMessage: messageId,
|
|
|
|
selectedMessageCounter: state.selectedMessageCounter + 1,
|
|
|
|
};
|
|
|
|
}
|
2019-05-31 22:42:01 +00:00
|
|
|
if (action.type === 'MESSAGE_CHANGED') {
|
|
|
|
const { id, conversationId, data } = action.payload;
|
|
|
|
const existingConversation = state.messagesByConversation[conversationId];
|
|
|
|
|
|
|
|
// We don't keep track of messages unless their conversation is loaded...
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
// ...and we've already loaded that message once
|
|
|
|
const existingMessage = state.messagesLookup[id];
|
|
|
|
if (!existingMessage) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for changes which could affect height - that's why we need this
|
|
|
|
// heightChangeMessageIds field. It tells Timeline to recalculate all of its heights
|
|
|
|
const hasHeightChanged = hasMessageHeightChanged(data, existingMessage);
|
|
|
|
|
|
|
|
const { heightChangeMessageIds } = existingConversation;
|
|
|
|
const updatedChanges = hasHeightChanged
|
|
|
|
? uniq([...heightChangeMessageIds, id])
|
|
|
|
: heightChangeMessageIds;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesLookup: {
|
|
|
|
...state.messagesLookup,
|
|
|
|
[id]: data,
|
|
|
|
},
|
|
|
|
messagesByConversation: {
|
|
|
|
...state.messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
heightChangeMessageIds: updatedChanges,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2020-12-07 20:43:19 +00:00
|
|
|
if (action.type === 'MESSAGE_SIZE_CHANGED') {
|
|
|
|
const { id, conversationId } = action.payload;
|
|
|
|
|
|
|
|
const existingConversation = getOwn(
|
|
|
|
state.messagesByConversation,
|
|
|
|
conversationId
|
|
|
|
);
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesByConversation: {
|
|
|
|
...state.messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
heightChangeMessageIds: uniq([
|
|
|
|
...existingConversation.heightChangeMessageIds,
|
|
|
|
id,
|
|
|
|
]),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2019-05-31 22:42:01 +00:00
|
|
|
if (action.type === 'MESSAGES_RESET') {
|
|
|
|
const {
|
|
|
|
conversationId,
|
|
|
|
messages,
|
|
|
|
metrics,
|
|
|
|
scrollToMessageId,
|
2020-07-06 17:06:44 +00:00
|
|
|
unboundedFetch,
|
2019-05-31 22:42:01 +00:00
|
|
|
} = action.payload;
|
|
|
|
const { messagesByConversation, messagesLookup } = state;
|
|
|
|
|
|
|
|
const existingConversation = messagesByConversation[conversationId];
|
|
|
|
const resetCounter = existingConversation
|
|
|
|
? existingConversation.resetCounter + 1
|
|
|
|
: 0;
|
|
|
|
|
2020-10-23 19:20:21 +00:00
|
|
|
const sorted = orderBy(
|
|
|
|
messages,
|
|
|
|
['received_at', 'sent_at'],
|
|
|
|
['ASC', 'ASC']
|
|
|
|
);
|
2019-05-31 22:42:01 +00:00
|
|
|
const messageIds = sorted.map(message => message.id);
|
|
|
|
|
|
|
|
const lookup = fromPairs(messages.map(message => [message.id, message]));
|
|
|
|
|
2020-01-16 16:45:06 +00:00
|
|
|
let { newest, oldest } = metrics;
|
|
|
|
|
|
|
|
// If our metrics are a little out of date, we'll fix them up
|
|
|
|
if (messages.length > 0) {
|
|
|
|
const first = messages[0];
|
2020-02-03 19:32:03 +00:00
|
|
|
if (first && (!oldest || first.received_at <= oldest.received_at)) {
|
2021-01-13 16:32:18 +00:00
|
|
|
oldest = pick(first, ['id', 'received_at', 'sent_at']);
|
2020-01-16 16:45:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const last = messages[messages.length - 1];
|
2020-07-06 17:06:44 +00:00
|
|
|
if (
|
|
|
|
last &&
|
|
|
|
(!newest || unboundedFetch || last.received_at >= newest.received_at)
|
|
|
|
) {
|
2021-01-13 16:32:18 +00:00
|
|
|
newest = pick(last, ['id', 'received_at', 'sent_at']);
|
2020-01-16 16:45:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
selectedMessage: scrollToMessageId,
|
|
|
|
selectedMessageCounter: state.selectedMessageCounter + 1,
|
|
|
|
messagesLookup: {
|
|
|
|
...messagesLookup,
|
|
|
|
...lookup,
|
|
|
|
},
|
|
|
|
messagesByConversation: {
|
|
|
|
...messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
isLoadingMessages: false,
|
|
|
|
scrollToMessageId,
|
2019-11-07 21:36:16 +00:00
|
|
|
scrollToMessageCounter: existingConversation
|
|
|
|
? existingConversation.scrollToMessageCounter + 1
|
|
|
|
: 0,
|
2019-05-31 22:42:01 +00:00
|
|
|
messageIds,
|
2020-01-16 16:45:06 +00:00
|
|
|
metrics: {
|
|
|
|
...metrics,
|
|
|
|
newest,
|
|
|
|
oldest,
|
|
|
|
},
|
2019-05-31 22:42:01 +00:00
|
|
|
resetCounter,
|
|
|
|
heightChangeMessageIds: [],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'SET_MESSAGES_LOADING') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { conversationId, isLoadingMessages } = payload;
|
|
|
|
|
|
|
|
const { messagesByConversation } = state;
|
|
|
|
const existingConversation = messagesByConversation[conversationId];
|
|
|
|
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesByConversation: {
|
|
|
|
...messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
loadCountdownStart: undefined,
|
|
|
|
isLoadingMessages,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'SET_LOAD_COUNTDOWN_START') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { conversationId, loadCountdownStart } = payload;
|
|
|
|
|
|
|
|
const { messagesByConversation } = state;
|
|
|
|
const existingConversation = messagesByConversation[conversationId];
|
|
|
|
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesByConversation: {
|
|
|
|
...messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
loadCountdownStart,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'SET_NEAR_BOTTOM') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { conversationId, isNearBottom } = payload;
|
|
|
|
|
|
|
|
const { messagesByConversation } = state;
|
|
|
|
const existingConversation = messagesByConversation[conversationId];
|
|
|
|
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesByConversation: {
|
|
|
|
...messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
isNearBottom,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'SCROLL_TO_MESSAGE') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { conversationId, messageId } = payload;
|
|
|
|
|
|
|
|
const { messagesByConversation, messagesLookup } = state;
|
|
|
|
const existingConversation = messagesByConversation[conversationId];
|
|
|
|
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
if (!messagesLookup[messageId]) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
if (!existingConversation.messageIds.includes(messageId)) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
selectedMessage: messageId,
|
|
|
|
selectedMessageCounter: state.selectedMessageCounter + 1,
|
|
|
|
messagesByConversation: {
|
|
|
|
...messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
isLoadingMessages: false,
|
|
|
|
scrollToMessageId: messageId,
|
|
|
|
scrollToMessageCounter:
|
|
|
|
existingConversation.scrollToMessageCounter + 1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'MESSAGE_DELETED') {
|
|
|
|
const { id, conversationId } = action.payload;
|
|
|
|
const { messagesByConversation, messagesLookup } = state;
|
|
|
|
|
|
|
|
const existingConversation = messagesByConversation[conversationId];
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Assuming that we always have contiguous groups of messages in memory, the removal
|
|
|
|
// of one message at one end of our message set be replaced with the message right
|
|
|
|
// next to it.
|
|
|
|
const oldIds = existingConversation.messageIds;
|
|
|
|
let { newest, oldest } = existingConversation.metrics;
|
|
|
|
|
|
|
|
if (oldIds.length > 1) {
|
|
|
|
const firstId = oldIds[0];
|
|
|
|
const lastId = oldIds[oldIds.length - 1];
|
|
|
|
|
|
|
|
if (oldest && oldest.id === firstId && firstId === id) {
|
|
|
|
const second = messagesLookup[oldIds[1]];
|
2021-01-13 16:32:18 +00:00
|
|
|
oldest = second
|
|
|
|
? pick(second, ['id', 'received_at', 'sent_at'])
|
|
|
|
: undefined;
|
2019-05-31 22:42:01 +00:00
|
|
|
}
|
|
|
|
if (newest && newest.id === lastId && lastId === id) {
|
|
|
|
const penultimate = messagesLookup[oldIds[oldIds.length - 2]];
|
|
|
|
newest = penultimate
|
2021-01-13 16:32:18 +00:00
|
|
|
? pick(penultimate, ['id', 'received_at', 'sent_at'])
|
2019-05-31 22:42:01 +00:00
|
|
|
: undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Removing it from our caches
|
|
|
|
const messageIds = without(existingConversation.messageIds, id);
|
|
|
|
const heightChangeMessageIds = without(
|
|
|
|
existingConversation.heightChangeMessageIds,
|
|
|
|
id
|
|
|
|
);
|
|
|
|
|
2020-09-09 02:25:05 +00:00
|
|
|
let metrics;
|
|
|
|
if (messageIds.length === 0) {
|
|
|
|
metrics = {
|
|
|
|
totalUnread: 0,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
metrics = {
|
|
|
|
...existingConversation.metrics,
|
|
|
|
oldest,
|
|
|
|
newest,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesLookup: omit(messagesLookup, id),
|
|
|
|
messagesByConversation: {
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
messageIds,
|
|
|
|
heightChangeMessageIds,
|
2020-09-09 02:25:05 +00:00
|
|
|
metrics,
|
2019-05-31 22:42:01 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2020-12-04 20:41:40 +00:00
|
|
|
|
|
|
|
if (action.type === 'REPAIR_NEWEST_MESSAGE') {
|
|
|
|
const { conversationId } = action.payload;
|
|
|
|
const { messagesByConversation, messagesLookup } = state;
|
|
|
|
|
|
|
|
const existingConversation = getOwn(messagesByConversation, conversationId);
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { messageIds } = existingConversation;
|
|
|
|
const lastId =
|
|
|
|
messageIds && messageIds.length
|
|
|
|
? messageIds[messageIds.length - 1]
|
|
|
|
: undefined;
|
|
|
|
const last = lastId ? getOwn(messagesLookup, lastId) : undefined;
|
2021-01-13 16:32:18 +00:00
|
|
|
const newest = last
|
|
|
|
? pick(last, ['id', 'received_at', 'sent_at'])
|
|
|
|
: undefined;
|
2020-12-04 20:41:40 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesByConversation: {
|
|
|
|
...messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
metrics: {
|
|
|
|
...existingConversation.metrics,
|
|
|
|
newest,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === 'REPAIR_OLDEST_MESSAGE') {
|
|
|
|
const { conversationId } = action.payload;
|
|
|
|
const { messagesByConversation, messagesLookup } = state;
|
|
|
|
|
|
|
|
const existingConversation = getOwn(messagesByConversation, conversationId);
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { messageIds } = existingConversation;
|
|
|
|
const firstId = messageIds && messageIds.length ? messageIds[0] : undefined;
|
|
|
|
const first = firstId ? getOwn(messagesLookup, firstId) : undefined;
|
2021-01-13 16:32:18 +00:00
|
|
|
const oldest = first
|
|
|
|
? pick(first, ['id', 'received_at', 'sent_at'])
|
|
|
|
: undefined;
|
2020-12-04 20:41:40 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesByConversation: {
|
|
|
|
...messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
metrics: {
|
|
|
|
...existingConversation.metrics,
|
|
|
|
oldest,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
if (action.type === 'MESSAGES_ADDED') {
|
2019-09-19 22:16:46 +00:00
|
|
|
const { conversationId, isActive, isNewMessage, messages } = action.payload;
|
2019-05-31 22:42:01 +00:00
|
|
|
const { messagesByConversation, messagesLookup } = state;
|
|
|
|
|
|
|
|
const existingConversation = messagesByConversation[conversationId];
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
let {
|
|
|
|
newest,
|
|
|
|
oldest,
|
|
|
|
oldestUnread,
|
|
|
|
totalUnread,
|
|
|
|
} = existingConversation.metrics;
|
|
|
|
|
2019-08-15 14:59:56 +00:00
|
|
|
if (messages.length < 1) {
|
|
|
|
return state;
|
2019-05-31 22:42:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const lookup = fromPairs(
|
|
|
|
existingConversation.messageIds.map(id => [id, messagesLookup[id]])
|
|
|
|
);
|
|
|
|
messages.forEach(message => {
|
|
|
|
lookup[message.id] = message;
|
|
|
|
});
|
|
|
|
|
2020-10-23 19:20:21 +00:00
|
|
|
const sorted = orderBy(
|
|
|
|
values(lookup),
|
|
|
|
['received_at', 'sent_at'],
|
|
|
|
['ASC', 'ASC']
|
|
|
|
);
|
2019-05-31 22:42:01 +00:00
|
|
|
const messageIds = sorted.map(message => message.id);
|
|
|
|
|
|
|
|
const first = sorted[0];
|
2019-08-15 14:59:56 +00:00
|
|
|
const last = sorted[sorted.length - 1];
|
|
|
|
|
|
|
|
if (!newest) {
|
2021-01-13 16:32:18 +00:00
|
|
|
newest = pick(first, ['id', 'received_at', 'sent_at']);
|
2019-08-15 14:59:56 +00:00
|
|
|
}
|
|
|
|
if (!oldest) {
|
2021-01-13 16:32:18 +00:00
|
|
|
oldest = pick(last, ['id', 'received_at', 'sent_at']);
|
2019-08-15 14:59:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const existingTotal = existingConversation.messageIds.length;
|
|
|
|
if (isNewMessage && existingTotal > 0) {
|
|
|
|
const lastMessageId = existingConversation.messageIds[existingTotal - 1];
|
|
|
|
|
|
|
|
// If our messages in memory don't include the most recent messages, then we
|
|
|
|
// won't add new messages to our message list.
|
|
|
|
const haveLatest = newest && newest.id === lastMessageId;
|
|
|
|
if (!haveLatest) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
}
|
2019-05-31 22:42:01 +00:00
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
// Update oldest and newest if we receive older/newer
|
|
|
|
// messages (or duplicated timestamps!)
|
2020-01-31 15:39:30 +00:00
|
|
|
if (first && oldest && first.received_at <= oldest.received_at) {
|
2021-01-13 16:32:18 +00:00
|
|
|
oldest = pick(first, ['id', 'received_at', 'sent_at']);
|
2019-05-31 22:42:01 +00:00
|
|
|
}
|
2020-01-31 15:39:30 +00:00
|
|
|
if (last && newest && last.received_at >= newest.received_at) {
|
2021-01-13 16:32:18 +00:00
|
|
|
newest = pick(last, ['id', 'received_at', 'sent_at']);
|
2019-05-31 22:42:01 +00:00
|
|
|
}
|
|
|
|
|
2019-08-15 14:59:56 +00:00
|
|
|
const newIds = messages.map(message => message.id);
|
2019-05-31 22:42:01 +00:00
|
|
|
const newMessageIds = difference(newIds, existingConversation.messageIds);
|
|
|
|
const { isNearBottom } = existingConversation;
|
|
|
|
|
2019-09-19 22:16:46 +00:00
|
|
|
if ((!isNearBottom || !isActive) && !oldestUnread) {
|
2019-05-31 22:42:01 +00:00
|
|
|
const oldestId = newMessageIds.find(messageId => {
|
|
|
|
const message = lookup[messageId];
|
|
|
|
|
|
|
|
return Boolean(message.unread);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (oldestId) {
|
|
|
|
oldestUnread = pick(lookup[oldestId], [
|
|
|
|
'id',
|
|
|
|
'received_at',
|
2021-01-13 16:32:18 +00:00
|
|
|
'sent_at',
|
2019-05-31 22:42:01 +00:00
|
|
|
]) as MessagePointerType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (oldestUnread) {
|
|
|
|
const newUnread: number = newMessageIds.reduce((sum, messageId) => {
|
|
|
|
const message = lookup[messageId];
|
|
|
|
|
|
|
|
return sum + (message && message.unread ? 1 : 0);
|
|
|
|
}, 0);
|
|
|
|
totalUnread = (totalUnread || 0) + newUnread;
|
|
|
|
}
|
|
|
|
|
2019-08-15 14:59:56 +00:00
|
|
|
const changedIds = intersection(newIds, existingConversation.messageIds);
|
|
|
|
const heightChangeMessageIds = uniq([
|
|
|
|
...changedIds,
|
|
|
|
...existingConversation.heightChangeMessageIds,
|
|
|
|
]);
|
|
|
|
|
2019-05-31 22:42:01 +00:00
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesLookup: {
|
|
|
|
...messagesLookup,
|
|
|
|
...lookup,
|
|
|
|
},
|
|
|
|
messagesByConversation: {
|
|
|
|
...messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
isLoadingMessages: false,
|
|
|
|
messageIds,
|
|
|
|
heightChangeMessageIds,
|
|
|
|
scrollToMessageId: undefined,
|
|
|
|
metrics: {
|
|
|
|
...existingConversation.metrics,
|
|
|
|
newest,
|
|
|
|
oldest,
|
|
|
|
totalUnread,
|
|
|
|
oldestUnread,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'CLEAR_SELECTED_MESSAGE') {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
selectedMessage: undefined,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'CLEAR_CHANGED_MESSAGES') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { conversationId } = payload;
|
|
|
|
const existingConversation = state.messagesByConversation[conversationId];
|
|
|
|
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesByConversation: {
|
|
|
|
...state.messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
heightChangeMessageIds: [],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'CLEAR_UNREAD_METRICS') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { conversationId } = payload;
|
|
|
|
const existingConversation = state.messagesByConversation[conversationId];
|
|
|
|
|
|
|
|
if (!existingConversation) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
messagesByConversation: {
|
|
|
|
...state.messagesByConversation,
|
|
|
|
[conversationId]: {
|
|
|
|
...existingConversation,
|
|
|
|
metrics: {
|
|
|
|
...existingConversation.metrics,
|
|
|
|
oldestUnread: undefined,
|
|
|
|
totalUnread: 0,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
2019-01-14 21:49:58 +00:00
|
|
|
}
|
2019-03-12 00:20:16 +00:00
|
|
|
if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
|
|
|
|
const { payload } = action;
|
|
|
|
const { id } = payload;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
selectedConversation: id,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'SHOW_INBOX') {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
showArchived: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
showArchived: true,
|
|
|
|
};
|
|
|
|
}
|
2019-01-14 21:49:58 +00:00
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
if (action.type === 'SET_CONVERSATION_HEADER_TITLE') {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
selectedConversationTitle: action.payload.title,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action.type === 'SET_RECENT_MEDIA_ITEMS') {
|
|
|
|
const { id, recentMediaItems } = action.payload;
|
|
|
|
const { conversationLookup } = state;
|
|
|
|
|
|
|
|
const conversationData = conversationLookup[id];
|
|
|
|
|
|
|
|
if (!conversationData) {
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
...conversationData,
|
|
|
|
recentMediaItems,
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
conversationLookup: {
|
|
|
|
...conversationLookup,
|
|
|
|
[id]: data,
|
|
|
|
},
|
|
|
|
...updateConversationLookups(data, undefined, state),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
return state;
|
|
|
|
}
|