signal-desktop/ts/state/ducks/conversations.ts

3756 lines
96 KiB
TypeScript
Raw Normal View History

// Copyright 2019-2022 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable camelcase */
import type { ThunkAction } from 'redux-thunk';
import {
difference,
fromPairs,
intersection,
omit,
orderBy,
pick,
uniq,
values,
without,
} from 'lodash';
import type { StateType as RootStateType } from '../reducer';
2021-03-03 20:09:58 +00:00
import * as groups from '../../groups';
2021-07-19 19:26:06 +00:00
import * as log from '../../logging/log';
import { calling } from '../../services/calling';
import { getOwn } from '../../util/getOwn';
2021-08-06 00:17:05 +00:00
import { assert, strictAssert } from '../../util/assert';
import * as universalExpireTimer from '../../util/universalExpireTimer';
2019-01-14 21:49:58 +00:00
import { trigger } from '../../shims/events';
2021-11-12 01:17:29 +00:00
import type {
ShowUsernameNotFoundModalActionType,
ToggleProfileEditorErrorActionType,
} from './globalModals';
import {
TOGGLE_PROFILE_EDITOR_ERROR,
actions as globalModalActions,
} from './globalModals';
import { isRecord } from '../../util/isRecord';
import type {
2021-05-28 16:15:17 +00:00
AvatarColorType,
ConversationColorType,
CustomColorType,
} from '../../types/Colors';
import type {
LastMessageStatus,
ConversationAttributesType,
MessageAttributesType,
} from '../../model-types.d';
import type { BodyRangeType } from '../../types/Util';
import { CallMode } from '../../types/Calling';
import type { MediaItemType } from '../../types/MediaItem';
2021-10-26 22:59:08 +00:00
import type { UUIDStringType } from '../../types/UUID';
2021-11-12 01:17:29 +00:00
import { UUID } from '../../types/UUID';
2021-03-03 20:09:58 +00:00
import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
} from '../../groups/limits';
import { isMessageUnread } from '../../util/isMessageUnread';
2021-03-11 21:29:31 +00:00
import { toggleSelectedContactForGroupAddition } from '../../groups/toggleSelectedContactForGroupAddition';
import type { GroupNameCollisionsWithIdsByTitle } from '../../util/groupMemberNameCollisions';
2021-06-01 23:30:25 +00:00
import { ContactSpoofingType } from '../../util/contactSpoofing';
2021-07-19 19:26:06 +00:00
import { writeProfile } from '../../services/writeProfile';
import { writeUsername } from '../../services/writeUsername';
import {
2021-11-12 01:17:29 +00:00
getConversationsByUsername,
2022-02-16 18:36:21 +00:00
getConversationIdsStoppingSend,
getConversationIdsStoppedForVerification,
getMe,
getUsernameSaveState,
} from '../selectors/conversations';
import type { AvatarDataType } from '../../types/Avatar';
import { getDefaultAvatars } from '../../types/Avatar';
2021-08-06 00:17:05 +00:00
import { getAvatarData } from '../../util/getAvatarData';
import { isSameAvatarData } from '../../util/isSameAvatarData';
2021-09-21 22:37:10 +00:00
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
import {
ComposerStep,
2022-02-16 18:36:21 +00:00
ConversationVerificationState,
OneTimeModalState,
2022-02-16 18:36:21 +00:00
UsernameSaveState,
} from './conversationsEnums';
import { showToast } from '../../util/showToast';
import { ToastFailedToDeleteUsername } from '../../components/ToastFailedToDeleteUsername';
2021-11-12 01:17:29 +00:00
import { ToastFailedToFetchUsername } from '../../components/ToastFailedToFetchUsername';
import { isValidUsername } from '../../types/Username';
2019-01-14 21:49:58 +00:00
import type { NoopActionType } from './noop';
2022-02-16 18:36:21 +00:00
import { conversationJobQueue } from '../../jobs/conversationJobQueue';
2019-01-14 21:49:58 +00:00
// State
export type DBConversationType = {
id: string;
activeAt?: number;
lastMessage?: string | null;
type: string;
};
export const InteractionModes = ['mouse', 'keyboard'] as const;
export type InteractionModeType = typeof InteractionModes[number];
export type MessageType = MessageAttributesType & {
interactionType?: InteractionModeType;
};
export type MessageWithUIFieldsType = MessageAttributesType & {
displayLimit?: number;
};
export const ConversationTypes = ['direct', 'group'] as const;
export type ConversationTypeType = typeof ConversationTypes[number];
2019-01-14 21:49:58 +00:00
export type ConversationType = {
id: string;
2021-10-26 22:59:08 +00:00
uuid?: UUIDStringType;
2020-07-24 01:35:32 +00:00
e164?: string;
2019-01-14 21:49:58 +00:00
name?: string;
2021-07-19 19:26:06 +00:00
familyName?: string;
firstName?: string;
2020-05-27 21:37:06 +00:00
profileName?: string;
username?: string;
2021-01-26 01:01:19 +00:00
about?: string;
2021-07-19 19:26:06 +00:00
aboutText?: string;
aboutEmoji?: string;
2021-08-06 00:17:05 +00:00
avatars?: Array<AvatarDataType>;
2020-05-27 21:37:06 +00:00
avatarPath?: string;
2021-07-19 19:26:06 +00:00
avatarHash?: string;
unblurredAvatarPath?: string;
areWeAdmin?: boolean;
areWePending?: boolean;
areWePendingApproval?: boolean;
2020-10-30 17:52:21 +00:00
canChangeTimer?: boolean;
canEditGroupInfo?: boolean;
2021-05-28 16:15:17 +00:00
color?: AvatarColorType;
conversationColor?: ConversationColorType;
customColor?: CustomColorType;
customColorId?: string;
discoveredUnregisteredAt?: number;
2020-05-27 21:37:06 +00:00
isArchived?: boolean;
isBlocked?: boolean;
isGroupV1AndDisabled?: boolean;
2021-03-03 20:09:58 +00:00
isGroupV2Capable?: boolean;
isPinned?: boolean;
isUntrusted?: boolean;
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;
lastMessage?:
| {
status?: LastMessageStatus;
text: string;
deletedForEveryone: false;
}
| { deletedForEveryone: true };
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;
messageCount?: number;
accessControlAddFromInviteLink?: number;
accessControlAttributes?: number;
accessControlMembers?: number;
2021-07-20 20:18:35 +00:00
announcementsOnly?: boolean;
announcementsOnlyReady?: boolean;
2020-10-30 17:52:21 +00:00
expireTimer?: number;
memberships?: Array<{
2021-10-26 22:59:08 +00:00
uuid: UUIDStringType;
isAdmin: boolean;
}>;
pendingMemberships?: Array<{
2021-10-26 22:59:08 +00:00
uuid: UUIDStringType;
addedByUserId?: UUIDStringType;
}>;
pendingApprovalMemberships?: Array<{
2021-10-26 22:59:08 +00:00
uuid: UUIDStringType;
}>;
2020-08-27 19:45:08 +00:00
muteExpiresAt?: number;
2021-08-05 12:35:33 +00:00
dontNotifyForMentionsIfMuted?: boolean;
type: ConversationTypeType;
2021-05-07 22:21:10 +00:00
isMe: boolean;
2020-11-20 17:30:45 +00:00
lastUpdated?: number;
// This is used by the CompositionInput for @mentions
sortedGroupMembers?: Array<ConversationType>;
title: string;
2021-04-27 22:35:35 +00:00
searchableTitle?: string;
unreadCount?: number;
isSelected?: boolean;
isFetchingUUID?: boolean;
typingContactId?: string;
recentMediaItems?: Array<MediaItemType>;
profileSharing?: boolean;
2019-08-07 00:40:25 +00:00
shouldShowDraft?: boolean;
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
2021-05-07 22:21:10 +00:00
sharedGroupNames: Array<string>;
2021-06-02 00:24:28 +00:00
groupDescription?: string;
groupVersion?: 1 | 2;
2020-11-13 19:57:55 +00:00
groupId?: string;
groupLink?: string;
2020-05-27 21:37:06 +00:00
messageRequestsEnabled?: boolean;
2021-05-07 22:21:10 +00:00
acceptedMessageRequest: boolean;
2020-11-13 19:57:55 +00:00
secretParams?: string;
publicParams?: string;
2021-06-01 23:30:25 +00:00
acknowledgedGroupNameCollisions?: GroupNameCollisionsWithIdsByTitle;
2021-07-19 19:26:06 +00:00
profileKey?: string;
2021-11-02 23:01:13 +00:00
badges: Array<
| {
id: string;
}
| {
id: string;
expiresAt: number;
isVisible: boolean;
}
>;
2019-01-14 21:49:58 +00:00
};
2021-07-19 19:26:06 +00:00
export type ProfileDataType = {
firstName: string;
} & Pick<ConversationType, 'aboutEmoji' | 'aboutText' | 'familyName'>;
2019-01-14 21:49:58 +00:00
export type ConversationLookupType = {
[key: string]: ConversationType;
};
export type CustomError = Error & {
identifier?: string;
number?: string;
};
type MessagePointerType = {
id: string;
received_at: number;
sent_at?: number;
};
type MessageMetricsType = {
newest?: MessagePointerType;
oldest?: MessagePointerType;
oldestUnread?: MessagePointerType;
totalUnread: number;
};
export type MessageLookupType = {
[key: string]: MessageWithUIFieldsType;
};
export type ConversationMessageType = {
heightChangeMessageIds: Array<string>;
isLoadingMessages: boolean;
isNearBottom?: boolean;
loadCountdownStart?: number;
messageIds: Array<string>;
metrics: MessageMetricsType;
resetCounter: number;
scrollToMessageId?: string;
scrollToMessageCounter: number;
};
export type MessagesByConversationType = {
[key: string]: ConversationMessageType | undefined;
};
2019-01-14 21:49:58 +00:00
export type PreJoinConversationType = {
avatar?: {
loading?: boolean;
url?: string;
};
2021-06-02 00:24:28 +00:00
groupDescription?: string;
memberCount: number;
title: string;
approvalRequired: boolean;
};
2021-03-03 20:09:58 +00:00
type ComposerGroupCreationState = {
2021-09-24 00:49:05 +00:00
groupAvatar: undefined | Uint8Array;
2021-03-03 20:09:58 +00:00
groupName: string;
groupExpireTimer: number;
2021-03-03 20:09:58 +00:00
maximumGroupSizeModalState: OneTimeModalState;
recommendedGroupSizeModalState: OneTimeModalState;
selectedConversationIds: Array<string>;
2021-08-06 00:17:05 +00:00
userAvatarData: Array<AvatarDataType>;
2021-03-03 20:09:58 +00:00
};
2022-02-16 18:36:21 +00:00
export type ConversationVerificationData =
| {
type: ConversationVerificationState.PendingVerification;
conversationsNeedingVerification: ReadonlyArray<string>;
}
| {
type: ConversationVerificationState.VerificationCancelled;
canceledAt: number;
};
2021-11-12 01:17:29 +00:00
export type FoundUsernameType = {
uuid: UUIDStringType;
username: string;
};
2021-03-03 20:09:58 +00:00
type ComposerStateType =
| {
step: ComposerStep.StartDirectConversation;
searchTerm: string;
2021-11-12 01:17:29 +00:00
isFetchingUsername: boolean;
2021-03-03 20:09:58 +00:00
}
| ({
step: ComposerStep.ChooseGroupMembers;
searchTerm: string;
2021-03-03 20:09:58 +00:00
cantAddContactIdForModal: undefined | string;
} & ComposerGroupCreationState)
| ({
step: ComposerStep.SetGroupMetadata;
isEditingAvatar: boolean;
2021-03-03 20:09:58 +00:00
} & ComposerGroupCreationState &
(
| { isCreating: false; hasError: boolean }
| { isCreating: true; hasError: false }
));
2021-06-01 23:30:25 +00:00
type ContactSpoofingReviewStateType =
| {
type: ContactSpoofingType.DirectConversationWithSameTitle;
safeConversationId: string;
}
| {
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
groupConversationId: string;
};
2021-04-21 16:31:12 +00:00
2019-01-14 21:49:58 +00:00
export type ConversationsStateType = {
preJoinConversation?: PreJoinConversationType;
2021-10-26 22:59:08 +00:00
invitedUuidsForNewlyCreatedGroup?: Array<string>;
2019-01-14 21:49:58 +00:00
conversationLookup: ConversationLookupType;
conversationsByE164: ConversationLookupType;
conversationsByUuid: ConversationLookupType;
conversationsByGroupId: ConversationLookupType;
2021-11-12 01:17:29 +00:00
conversationsByUsername: ConversationLookupType;
selectedConversationId?: string;
selectedMessage?: string;
selectedMessageCounter: number;
selectedConversationTitle?: string;
2020-10-30 17:52:21 +00:00
selectedConversationPanelDepth: number;
2019-03-12 00:20:16 +00:00
showArchived: boolean;
2021-03-03 20:09:58 +00:00
composer?: ComposerStateType;
2021-04-21 16:31:12 +00:00
contactSpoofingReview?: ContactSpoofingReviewStateType;
usernameSaveState: UsernameSaveState;
/**
2022-02-16 18:36:21 +00:00
* Each key is a conversation ID. Each value is a value representing the state of
* verification: either a set of pending conversationIds to be approved, or a tombstone
* telling jobs to cancel themselves up to that timestamp.
*/
2022-02-16 18:36:21 +00:00
verificationDataByConversation: Record<string, ConversationVerificationData>;
// 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
2022-02-16 18:36:21 +00:00
const CANCEL_CONVERSATION_PENDING_VERIFICATION =
'conversations/CANCEL_CONVERSATION_PENDING_VERIFICATION';
const CLEAR_CANCELLED_VERIFICATION =
'conversations/CLEAR_CANCELLED_VERIFICATION';
const CLEAR_CONVERSATIONS_PENDING_VERIFICATION =
'conversations/CLEAR_CONVERSATIONS_PENDING_VERIFICATION';
2021-08-18 20:08:14 +00:00
export const COLORS_CHANGED = 'conversations/COLORS_CHANGED';
export const COLOR_SELECTED = 'conversations/COLOR_SELECTED';
2021-08-06 00:17:05 +00:00
const COMPOSE_TOGGLE_EDITING_AVATAR =
'conversations/compose/COMPOSE_TOGGLE_EDITING_AVATAR';
const COMPOSE_ADD_AVATAR = 'conversations/compose/ADD_AVATAR';
const COMPOSE_REMOVE_AVATAR = 'conversations/compose/REMOVE_AVATAR';
const COMPOSE_REPLACE_AVATAR = 'conversations/compose/REPLACE_AVATAR';
2021-05-28 16:15:17 +00:00
const CUSTOM_COLOR_REMOVED = 'conversations/CUSTOM_COLOR_REMOVED';
2022-02-16 18:36:21 +00:00
const CONVERSATION_STOPPED_BY_MISSING_VERIFICATION =
'conversations/CONVERSATION_STOPPED_BY_MISSING_VERIFICATION';
2021-08-06 00:17:05 +00:00
const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS';
const UPDATE_USERNAME_SAVE_STATE = 'conversations/UPDATE_USERNAME_SAVE_STATE';
2021-05-28 16:15:17 +00:00
2022-02-16 18:36:21 +00:00
export type CancelVerificationDataByConversationActionType = {
type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION;
payload: {
canceledAt: number;
};
};
2021-03-03 20:09:58 +00:00
type CantAddContactToGroupActionType = {
type: 'CANT_ADD_CONTACT_TO_GROUP';
payload: {
conversationId: string;
};
};
type ClearGroupCreationErrorActionType = { type: 'CLEAR_GROUP_CREATION_ERROR' };
2021-10-26 22:59:08 +00:00
type ClearInvitedUuidsForNewlyCreatedGroupActionType = {
type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP';
2021-03-03 20:09:58 +00:00
};
2022-02-16 18:36:21 +00:00
type ClearVerificationDataByConversationActionType = {
type: typeof CLEAR_CONVERSATIONS_PENDING_VERIFICATION;
};
type ClearCancelledVerificationActionType = {
type: typeof CLEAR_CANCELLED_VERIFICATION;
payload: {
conversationId: string;
};
2022-01-25 01:39:18 +00:00
};
2021-03-03 20:09:58 +00:00
type CloseCantAddContactToGroupModalActionType = {
type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL';
};
2021-04-21 16:31:12 +00:00
type CloseContactSpoofingReviewActionType = {
type: 'CLOSE_CONTACT_SPOOFING_REVIEW';
};
2021-03-03 20:09:58 +00:00
type CloseMaximumGroupSizeModalActionType = {
type: 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL';
};
type CloseRecommendedGroupSizeModalActionType = {
type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL';
};
2021-05-28 16:15:17 +00:00
type ColorsChangedActionType = {
type: typeof COLORS_CHANGED;
payload: {
conversationColor?: ConversationColorType;
customColorData?: {
id: string;
value: CustomColorType;
};
};
};
2021-06-03 21:34:36 +00:00
type ColorSelectedPayloadType = {
conversationId: string;
conversationColor?: ConversationColorType;
customColorData?: {
id: string;
value: CustomColorType;
};
};
export type ColorSelectedActionType = {
type: typeof COLOR_SELECTED;
payload: ColorSelectedPayloadType;
};
2021-08-06 00:17:05 +00:00
type ComposeDeleteAvatarActionType = {
type: typeof COMPOSE_REMOVE_AVATAR;
payload: AvatarDataType;
};
type ComposeReplaceAvatarsActionType = {
type: typeof COMPOSE_REPLACE_AVATAR;
payload: {
curr: AvatarDataType;
prev?: AvatarDataType;
};
};
type ComposeSaveAvatarActionType = {
type: typeof COMPOSE_ADD_AVATAR;
payload: AvatarDataType;
};
2021-05-28 16:15:17 +00:00
type CustomColorRemovedActionType = {
type: typeof CUSTOM_COLOR_REMOVED;
2021-06-02 21:05:09 +00:00
payload: {
colorId: string;
};
2021-05-28 16:15:17 +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;
};
};
2021-09-02 22:34:38 +00:00
export type ConversationChangedActionType = {
2019-01-14 21:49:58 +00:00
type: 'CONVERSATION_CHANGED';
payload: {
id: string;
data: ConversationType;
};
};
export type ConversationRemovedActionType = {
2019-01-14 21:49:58 +00:00
type: 'CONVERSATION_REMOVED';
payload: {
id: string;
};
};
2019-11-07 21:36:16 +00:00
export type ConversationUnloadedActionType = {
type: 'CONVERSATION_UNLOADED';
payload: {
id: string;
};
};
2021-03-03 20:09:58 +00:00
type CreateGroupPendingActionType = {
type: 'CREATE_GROUP_PENDING';
};
type CreateGroupFulfilledActionType = {
type: 'CREATE_GROUP_FULFILLED';
payload: {
2021-10-26 22:59:08 +00:00
invitedUuids: Array<UUIDStringType>;
2021-03-03 20:09:58 +00:00
};
};
type CreateGroupRejectedActionType = {
type: 'CREATE_GROUP_REJECTED';
};
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;
};
};
2022-02-16 18:36:21 +00:00
type ConversationStoppedByMissingVerificationActionType = {
type: typeof CONVERSATION_STOPPED_BY_MISSING_VERIFICATION;
payload: {
2022-02-16 18:36:21 +00:00
conversationId: string;
untrustedConversationIds: ReadonlyArray<string>;
};
};
export type MessageChangedActionType = {
type: 'MESSAGE_CHANGED';
2019-01-14 21:49:58 +00:00
payload: {
id: string;
conversationId: string;
data: MessageAttributesType;
};
};
export type MessageDeletedActionType = {
type: 'MESSAGE_DELETED';
payload: {
id: string;
conversationId: string;
};
};
export type MessageExpandedActionType = {
type: 'MESSAGE_EXPANDED';
payload: {
id: string;
displayLimit: number;
};
};
type MessageSizeChangedActionType = {
type: 'MESSAGE_SIZE_CHANGED';
payload: {
id: string;
conversationId: string;
};
};
export type MessagesAddedActionType = {
type: 'MESSAGES_ADDED';
payload: {
conversationId: string;
isActive: boolean;
isJustSent: boolean;
isNewMessage: boolean;
messages: Array<MessageAttributesType>;
};
};
export type RepairNewestMessageActionType = {
type: 'REPAIR_NEWEST_MESSAGE';
payload: {
conversationId: string;
};
};
export type RepairOldestMessageActionType = {
type: 'REPAIR_OLDEST_MESSAGE';
payload: {
conversationId: string;
};
};
export type MessagesResetActionType = {
type: 'MESSAGES_RESET';
payload: {
conversationId: string;
messages: Array<MessageAttributesType>;
metrics: MessageMetricsType;
scrollToMessageId?: string;
// 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;
};
};
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;
};
};
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 };
};
export type ScrollToMessageActionType = {
type: 'SCROLL_TO_MESSAGE';
payload: {
conversationId: string;
messageId: string;
};
};
export type ClearChangedMessagesActionType = {
type: 'CLEAR_CHANGED_MESSAGES';
payload: {
conversationId: string;
baton: unknown;
};
};
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;
};
};
2021-06-01 23:30:25 +00:00
type ReviewGroupMemberNameCollisionActionType = {
type: 'REVIEW_GROUP_MEMBER_NAME_COLLISION';
payload: {
groupConversationId: string;
};
};
2021-04-21 16:31:12 +00:00
type ReviewMessageRequestNameCollisionActionType = {
type: 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION';
payload: {
safeConversationId: 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-03-03 20:09:58 +00:00
type SetComposeGroupAvatarActionType = {
type: 'SET_COMPOSE_GROUP_AVATAR';
2021-09-24 00:49:05 +00:00
payload: { groupAvatar: undefined | Uint8Array };
2021-03-03 20:09:58 +00:00
};
type SetComposeGroupNameActionType = {
type: 'SET_COMPOSE_GROUP_NAME';
payload: { groupName: string };
};
type SetComposeGroupExpireTimerActionType = {
type: 'SET_COMPOSE_GROUP_EXPIRE_TIMER';
payload: { groupExpireTimer: number };
};
type SetComposeSearchTermActionType = {
type: 'SET_COMPOSE_SEARCH_TERM';
payload: { searchTerm: string };
};
2021-11-12 01:17:29 +00:00
type SetIsFetchingUsernameActionType = {
type: 'SET_IS_FETCHING_USERNAME';
payload: {
isFetchingUsername: boolean;
};
};
type SetRecentMediaItemsActionType = {
type: 'SET_RECENT_MEDIA_ITEMS';
payload: {
id: string;
recentMediaItems: Array<MediaItemType>;
};
};
2021-08-06 00:17:05 +00:00
type ToggleComposeEditingAvatarActionType = {
type: typeof COMPOSE_TOGGLE_EDITING_AVATAR;
};
type StartComposingActionType = {
type: 'START_COMPOSING';
};
2021-03-03 20:09:58 +00:00
type ShowChooseGroupMembersActionType = {
type: 'SHOW_CHOOSE_GROUP_MEMBERS';
};
type StartSettingGroupMetadataActionType = {
type: 'START_SETTING_GROUP_METADATA';
};
export type SwitchToAssociatedViewActionType = {
type: 'SWITCH_TO_ASSOCIATED_VIEW';
payload: { conversationId: string };
};
2021-03-03 20:09:58 +00:00
export type ToggleConversationInChooseMembersActionType = {
type: 'TOGGLE_CONVERSATION_IN_CHOOSE_MEMBERS';
payload: {
conversationId: string;
maxRecommendedGroupSize: number;
maxGroupSize: number;
};
};
type UpdateUsernameSaveStateActionType = {
type: typeof UPDATE_USERNAME_SAVE_STATE;
payload: {
newSaveState: UsernameSaveState;
};
};
2021-08-06 00:17:05 +00:00
type ReplaceAvatarsActionType = {
type: typeof REPLACE_AVATARS;
payload: {
conversationId: string;
avatars: Array<AvatarDataType>;
};
};
2019-01-14 21:49:58 +00:00
export type ConversationActionType =
2022-02-16 18:36:21 +00:00
| CancelVerificationDataByConversationActionType
2021-03-03 20:09:58 +00:00
| CantAddContactToGroupActionType
2022-02-16 18:36:21 +00:00
| ClearCancelledVerificationActionType
| ClearChangedMessagesActionType
2022-02-16 18:36:21 +00:00
| ClearVerificationDataByConversationActionType
2021-03-03 20:09:58 +00:00
| ClearGroupCreationErrorActionType
2021-10-26 22:59:08 +00:00
| ClearInvitedUuidsForNewlyCreatedGroupActionType
| ClearSelectedMessageActionType
| ClearUnreadMetricsActionType
2021-03-03 20:09:58 +00:00
| CloseCantAddContactToGroupModalActionType
2021-04-21 16:31:12 +00:00
| CloseContactSpoofingReviewActionType
2021-03-03 20:09:58 +00:00
| CloseMaximumGroupSizeModalActionType
| CloseRecommendedGroupSizeModalActionType
2021-08-06 00:17:05 +00:00
| ColorSelectedActionType
| ColorsChangedActionType
| ComposeDeleteAvatarActionType
| ComposeReplaceAvatarsActionType
| ComposeSaveAvatarActionType
2019-01-14 21:49:58 +00:00
| ConversationAddedActionType
| ConversationChangedActionType
| ConversationRemovedActionType
2022-02-16 18:36:21 +00:00
| ConversationStoppedByMissingVerificationActionType
| ConversationUnloadedActionType
2021-03-03 20:09:58 +00:00
| CreateGroupFulfilledActionType
| CreateGroupPendingActionType
| CreateGroupRejectedActionType
2021-08-06 00:17:05 +00:00
| CustomColorRemovedActionType
| MessageChangedActionType
| MessageDeletedActionType
| MessageExpandedActionType
| MessageSelectedActionType
| MessageSizeChangedActionType
2021-08-06 00:17:05 +00:00
| MessagesAddedActionType
| MessagesResetActionType
| RemoveAllConversationsActionType
| RepairNewestMessageActionType
| RepairOldestMessageActionType
2021-08-06 00:17:05 +00:00
| ReplaceAvatarsActionType
2021-06-01 23:30:25 +00:00
| ReviewGroupMemberNameCollisionActionType
2021-04-21 16:31:12 +00:00
| ReviewMessageRequestNameCollisionActionType
| ScrollToMessageActionType
2019-03-12 00:20:16 +00:00
| SelectedConversationChangedActionType
2021-03-03 20:09:58 +00:00
| SetComposeGroupAvatarActionType
| SetComposeGroupExpireTimerActionType
2021-08-06 00:17:05 +00:00
| SetComposeGroupNameActionType
| SetComposeSearchTermActionType
| SetConversationHeaderTitleActionType
2021-11-12 01:17:29 +00:00
| SetIsFetchingUsernameActionType
| SetIsNearBottomActionType
| SetLoadCountdownStartActionType
| SetMessagesLoadingActionType
| SetPreJoinConversationActionType
| SetRecentMediaItemsActionType
| SetSelectedConversationPanelDepthActionType
| ShowArchivedConversationsActionType
2021-08-06 00:17:05 +00:00
| ShowChooseGroupMembersActionType
| ShowInboxActionType
| StartComposingActionType
2021-03-03 20:09:58 +00:00
| StartSettingGroupMetadataActionType
| SwitchToAssociatedViewActionType
2021-08-06 00:17:05 +00:00
| ToggleConversationInChooseMembersActionType
| ToggleComposeEditingAvatarActionType
| UpdateUsernameSaveStateActionType;
2019-01-14 21:49:58 +00:00
// Action Creators
export const actions = {
2022-02-16 18:36:21 +00:00
cancelConversationVerification,
2021-03-03 20:09:58 +00:00
cantAddContactToGroup,
2022-02-16 18:36:21 +00:00
clearCancelledConversationVerification,
clearChangedMessages,
2021-03-03 20:09:58 +00:00
clearGroupCreationError,
2021-10-26 22:59:08 +00:00
clearInvitedUuidsForNewlyCreatedGroup,
clearSelectedMessage,
clearUnreadMetrics,
clearUsernameSave,
2021-03-03 20:09:58 +00:00
closeCantAddContactToGroupModal,
2021-04-21 16:31:12 +00:00
closeContactSpoofingReview,
2021-03-03 20:09:58 +00:00
closeMaximumGroupSizeModal,
2021-08-06 00:17:05 +00:00
closeRecommendedGroupSizeModal,
colorSelected,
composeDeleteAvatarFromDisk,
composeReplaceAvatar,
composeSaveAvatarToDisk,
2019-01-14 21:49:58 +00:00
conversationAdded,
conversationChanged,
conversationRemoved,
2022-02-16 18:36:21 +00:00
conversationStoppedByMissingVerification,
conversationUnloaded,
2021-03-03 20:09:58 +00:00
createGroup,
2021-08-06 00:17:05 +00:00
deleteAvatarFromDisk,
doubleCheckMissingQuoteReference,
messageChanged,
messageDeleted,
messageExpanded,
messageSizeChanged,
2021-08-06 00:17:05 +00:00
messagesAdded,
messagesReset,
2021-07-19 19:26:06 +00:00
myProfileChanged,
2019-01-14 21:49:58 +00:00
openConversationExternal,
openConversationInternal,
removeAllConversations,
2021-05-28 16:15:17 +00:00
removeCustomColorOnConversations,
2021-09-21 22:37:10 +00:00
removeMemberFromGroup,
repairNewestMessage,
repairOldestMessage,
2021-08-06 00:17:05 +00:00
replaceAvatar,
2021-05-28 16:15:17 +00:00
resetAllChatColors,
2021-06-01 23:30:25 +00:00
reviewGroupMemberNameCollision,
2021-04-21 16:31:12 +00:00
reviewMessageRequestNameCollision,
2021-08-06 00:17:05 +00:00
saveAvatarToDisk,
saveUsername,
scrollToMessage,
selectMessage,
2021-03-03 20:09:58 +00:00
setComposeGroupAvatar,
setComposeGroupExpireTimer,
2021-08-06 00:17:05 +00:00
setComposeGroupName,
setComposeSearchTerm,
setIsNearBottom,
setLoadCountdownStart,
setMessagesLoading,
setPreJoinConversation,
setRecentMediaItems,
setSelectedConversationHeaderTitle,
setSelectedConversationPanelDepth,
showArchivedConversations,
2021-08-06 00:17:05 +00:00
showChooseGroupMembers,
showInbox,
startComposing,
startNewConversationFromPhoneNumber,
2021-11-12 01:17:29 +00:00
startNewConversationFromUsername,
2021-03-03 20:09:58 +00:00
startSettingGroupMetadata,
2021-09-21 22:37:10 +00:00
toggleAdmin,
2021-03-03 20:09:58 +00:00
toggleConversationInChooseMembers,
2021-08-06 00:17:05 +00:00
toggleComposeEditingAvatar,
2021-09-21 22:37:10 +00:00
updateConversationModelSharedGroups,
2022-02-16 18:36:21 +00:00
verifyConversationsStoppingSend,
2019-01-14 21:49:58 +00:00
};
2021-08-06 00:17:05 +00:00
function filterAvatarData(
avatars: ReadonlyArray<AvatarDataType>,
data: AvatarDataType
): Array<AvatarDataType> {
return avatars.filter(avatarData => !isSameAvatarData(data, avatarData));
}
function getNextAvatarId(avatars: Array<AvatarDataType>): number {
return Math.max(...avatars.map(x => Number(x.id))) + 1;
}
async function getAvatarsAndUpdateConversation(
conversations: ConversationsStateType,
conversationId: string,
getNextAvatarsData: (
avatars: Array<AvatarDataType>,
nextId: number
) => Array<AvatarDataType>
): Promise<Array<AvatarDataType>> {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('No conversation found');
}
const { conversationLookup } = conversations;
const conversationAttrs = conversationLookup[conversationId];
const avatars =
conversationAttrs.avatars || getAvatarData(conversation.attributes);
const nextAvatarId = getNextAvatarId(avatars);
const nextAvatars = getNextAvatarsData(avatars, nextAvatarId);
// We don't save buffers to the db, but we definitely want it in-memory so
// we don't have to re-generate them.
//
// Mutating here because we don't want to trigger a model change
// because we're updating redux here manually ourselves. Au revoir Backbone!
conversation.attributes.avatars = nextAvatars.map(avatarData =>
omit(avatarData, ['buffer'])
);
await window.Signal.Data.updateConversation(conversation.attributes);
return nextAvatars;
}
function deleteAvatarFromDisk(
avatarData: AvatarDataType,
conversationId?: string
): ThunkAction<void, RootStateType, unknown, ReplaceAvatarsActionType> {
return async (dispatch, getState) => {
if (avatarData.imagePath) {
await window.Signal.Migrations.deleteAvatar(avatarData.imagePath);
} else {
log.info(
2021-08-06 00:17:05 +00:00
'No imagePath for avatarData. Removing from userAvatarData, but not disk'
);
}
strictAssert(conversationId, 'conversationId not provided');
const avatars = await getAvatarsAndUpdateConversation(
getState().conversations,
conversationId,
prevAvatarsData => filterAvatarData(prevAvatarsData, avatarData)
);
dispatch({
type: REPLACE_AVATARS,
payload: {
conversationId,
avatars,
},
});
};
}
function replaceAvatar(
curr: AvatarDataType,
prev?: AvatarDataType,
conversationId?: string
): ThunkAction<void, RootStateType, unknown, ReplaceAvatarsActionType> {
return async (dispatch, getState) => {
strictAssert(conversationId, 'conversationId not provided');
const avatars = await getAvatarsAndUpdateConversation(
getState().conversations,
conversationId,
(prevAvatarsData, nextId) => {
const newAvatarData = {
...curr,
id: prev?.id ?? nextId,
};
const existingAvatarsData = prev
? filterAvatarData(prevAvatarsData, prev)
: prevAvatarsData;
return [newAvatarData, ...existingAvatarsData];
}
);
dispatch({
type: REPLACE_AVATARS,
payload: {
conversationId,
avatars,
},
});
};
}
function saveAvatarToDisk(
avatarData: AvatarDataType,
conversationId?: string
): ThunkAction<void, RootStateType, unknown, ReplaceAvatarsActionType> {
return async (dispatch, getState) => {
if (!avatarData.buffer) {
2021-09-24 00:49:05 +00:00
throw new Error('No avatar Uint8Array provided');
2021-08-06 00:17:05 +00:00
}
strictAssert(conversationId, 'conversationId not provided');
const imagePath = await window.Signal.Migrations.writeNewAvatarData(
avatarData.buffer
);
const avatars = await getAvatarsAndUpdateConversation(
getState().conversations,
conversationId,
(prevAvatarsData, id) => {
const newAvatarData = {
...avatarData,
imagePath,
id,
};
return [newAvatarData, ...prevAvatarsData];
}
);
dispatch({
type: REPLACE_AVATARS,
payload: {
conversationId,
avatars,
},
});
};
}
function makeUsernameSaveType(
newSaveState: UsernameSaveState
): UpdateUsernameSaveStateActionType {
return {
type: UPDATE_USERNAME_SAVE_STATE,
payload: {
newSaveState,
},
};
}
function clearUsernameSave(): UpdateUsernameSaveStateActionType {
return makeUsernameSaveType(UsernameSaveState.None);
}
function saveUsername({
username,
previousUsername,
}: {
username: string | undefined;
previousUsername: string | undefined;
}): ThunkAction<
void,
RootStateType,
unknown,
UpdateUsernameSaveStateActionType
> {
return async (dispatch, getState) => {
const state = getState();
const previousState = getUsernameSaveState(state);
if (previousState !== UsernameSaveState.None) {
log.error(
`saveUsername: Save requested, but previous state was ${previousState}`
);
dispatch(makeUsernameSaveType(UsernameSaveState.GeneralError));
return;
}
try {
dispatch(makeUsernameSaveType(UsernameSaveState.Saving));
await writeUsername({ username, previousUsername });
// writeUsername above updates the backbone model which in turn updates
// redux through it's on:change event listener. Once we lose Backbone
// we'll need to manually sync these new changes.
dispatch(makeUsernameSaveType(UsernameSaveState.Success));
} catch (error: unknown) {
// Check to see if we were deleting
if (!username) {
dispatch(makeUsernameSaveType(UsernameSaveState.DeleteFailed));
showToast(ToastFailedToDeleteUsername);
return;
}
if (!isRecord(error)) {
dispatch(makeUsernameSaveType(UsernameSaveState.GeneralError));
return;
}
if (error.code === 409) {
dispatch(makeUsernameSaveType(UsernameSaveState.UsernameTakenError));
return;
}
if (error.code === 400) {
dispatch(
makeUsernameSaveType(UsernameSaveState.UsernameMalformedError)
);
return;
}
dispatch(makeUsernameSaveType(UsernameSaveState.GeneralError));
}
};
}
2021-07-19 19:26:06 +00:00
function myProfileChanged(
profileData: ProfileDataType,
2021-09-24 00:49:05 +00:00
avatarBuffer?: Uint8Array
2021-07-19 19:26:06 +00:00
): ThunkAction<
void,
RootStateType,
unknown,
NoopActionType | ToggleProfileEditorErrorActionType
> {
return async (dispatch, getState) => {
const conversation = getMe(getState());
try {
await writeProfile(
{
...conversation,
...profileData,
},
2021-08-06 00:17:05 +00:00
avatarBuffer
2021-07-19 19:26:06 +00:00
);
// writeProfile above updates the backbone model which in turn updates
// redux through it's on:change event listener. Once we lose Backbone
// we'll need to manually sync these new changes.
dispatch({
type: 'NOOP',
payload: null,
});
} catch (err) {
log.error('myProfileChanged', err && err.stack ? err.stack : err);
dispatch({ type: TOGGLE_PROFILE_EDITOR_ERROR });
}
};
}
2021-05-28 16:15:17 +00:00
function removeCustomColorOnConversations(
colorId: string
): ThunkAction<void, RootStateType, unknown, CustomColorRemovedActionType> {
return async dispatch => {
const conversationsToUpdate: Array<ConversationAttributesType> = [];
// We don't want to trigger a model change because we're updating redux
// here manually ourselves. Au revoir Backbone!
window.getConversations().forEach(conversation => {
if (conversation.get('customColorId') === colorId) {
// eslint-disable-next-line no-param-reassign
delete conversation.attributes.conversationColor;
// eslint-disable-next-line no-param-reassign
delete conversation.attributes.customColor;
// eslint-disable-next-line no-param-reassign
delete conversation.attributes.customColorId;
conversationsToUpdate.push(conversation.attributes);
}
});
if (conversationsToUpdate.length) {
await window.Signal.Data.updateConversations(conversationsToUpdate);
}
dispatch({
type: CUSTOM_COLOR_REMOVED,
2021-06-02 21:05:09 +00:00
payload: {
colorId,
},
2021-05-28 16:15:17 +00:00
});
};
}
function resetAllChatColors(): ThunkAction<
void,
RootStateType,
unknown,
ColorsChangedActionType
> {
return async dispatch => {
// Calling this with no args unsets all the colors in the db
await window.Signal.Data.updateAllConversationColors();
// We don't want to trigger a model change because we're updating redux
// here manually ourselves. Au revoir Backbone!
window.getConversations().forEach(conversation => {
// eslint-disable-next-line no-param-reassign
delete conversation.attributes.conversationColor;
// eslint-disable-next-line no-param-reassign
delete conversation.attributes.customColor;
// eslint-disable-next-line no-param-reassign
delete conversation.attributes.customColorId;
});
dispatch({
type: COLORS_CHANGED,
payload: {
conversationColor: undefined,
customColorData: undefined,
2021-05-28 16:15:17 +00:00
},
});
};
}
2021-06-03 21:34:36 +00:00
function colorSelected({
conversationId,
conversationColor,
customColorData,
}: ColorSelectedPayloadType): ThunkAction<
void,
RootStateType,
unknown,
ColorSelectedActionType
> {
return async dispatch => {
// We don't want to trigger a model change because we're updating redux
// here manually ourselves. Au revoir Backbone!
const conversation = window.ConversationController.get(conversationId);
if (conversation) {
if (conversationColor) {
conversation.attributes.conversationColor = conversationColor;
if (customColorData) {
conversation.attributes.customColor = customColorData.value;
conversation.attributes.customColorId = customColorData.id;
} else {
delete conversation.attributes.customColor;
delete conversation.attributes.customColorId;
}
} else {
delete conversation.attributes.conversationColor;
delete conversation.attributes.customColor;
delete conversation.attributes.customColorId;
}
await window.Signal.Data.updateConversation(conversation.attributes);
}
dispatch({
type: COLOR_SELECTED,
payload: {
conversationId,
conversationColor,
customColorData,
},
});
};
}
2021-08-06 00:17:05 +00:00
function toggleComposeEditingAvatar(): ToggleComposeEditingAvatarActionType {
return {
type: COMPOSE_TOGGLE_EDITING_AVATAR,
};
}
2022-02-16 18:36:21 +00:00
export function cancelConversationVerification(
canceledAt?: number
): ThunkAction<
void,
RootStateType,
unknown,
2022-02-16 18:36:21 +00:00
CancelVerificationDataByConversationActionType
> {
2022-02-16 18:36:21 +00:00
return (dispatch, getState) => {
const state = getState();
const conversationIdsBlocked =
getConversationIdsStoppedForVerification(state);
2022-01-25 01:39:18 +00:00
2022-02-16 18:36:21 +00:00
dispatch({
type: CANCEL_CONVERSATION_PENDING_VERIFICATION,
payload: {
canceledAt: canceledAt ?? Date.now(),
},
});
2022-01-25 01:39:18 +00:00
2022-02-16 18:36:21 +00:00
// Start the blocked conversation queues up again
conversationIdsBlocked.forEach(conversationId => {
conversationJobQueue.resolveVerificationWaiter(conversationId);
});
};
}
2022-02-16 18:36:21 +00:00
function verifyConversationsStoppingSend(): ThunkAction<
void,
RootStateType,
unknown,
ClearVerificationDataByConversationActionType
> {
return async (dispatch, getState) => {
const state = getState();
const conversationIdsStoppingSend = getConversationIdsStoppingSend(state);
const conversationIdsBlocked =
getConversationIdsStoppedForVerification(state);
2022-01-25 01:39:18 +00:00
2022-02-16 18:36:21 +00:00
// Mark conversations as approved/verified as appropriate
const promises: Array<Promise<unknown>> = [];
conversationIdsStoppingSend.forEach(async conversationId => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
return;
}
if (conversation.isUnverified()) {
promises.push(conversation.setVerifiedDefault());
}
promises.push(conversation.setApproved());
});
2022-01-25 01:39:18 +00:00
dispatch({
2022-02-16 18:36:21 +00:00
type: CLEAR_CONVERSATIONS_PENDING_VERIFICATION,
2022-01-25 01:39:18 +00:00
});
await Promise.all(promises);
2022-02-16 18:36:21 +00:00
// Start the blocked conversation queues up again
conversationIdsBlocked.forEach(conversationId => {
conversationJobQueue.resolveVerificationWaiter(conversationId);
});
};
}
export function clearCancelledConversationVerification(
conversationId: string
): ClearCancelledVerificationActionType {
return {
type: CLEAR_CANCELLED_VERIFICATION,
payload: {
conversationId,
},
};
}
2021-08-06 00:17:05 +00:00
function composeSaveAvatarToDisk(
avatarData: AvatarDataType
): ThunkAction<void, RootStateType, unknown, ComposeSaveAvatarActionType> {
return async dispatch => {
if (!avatarData.buffer) {
2021-09-24 00:49:05 +00:00
throw new Error('No avatar Uint8Array provided');
2021-08-06 00:17:05 +00:00
}
const imagePath = await window.Signal.Migrations.writeNewAvatarData(
avatarData.buffer
);
dispatch({
type: COMPOSE_ADD_AVATAR,
payload: {
...avatarData,
imagePath,
},
});
};
}
function composeDeleteAvatarFromDisk(
avatarData: AvatarDataType
): ThunkAction<void, RootStateType, unknown, ComposeDeleteAvatarActionType> {
return async dispatch => {
if (avatarData.imagePath) {
await window.Signal.Migrations.deleteAvatar(avatarData.imagePath);
} else {
log.info(
2021-08-06 00:17:05 +00:00
'No imagePath for avatarData. Removing from userAvatarData, but not disk'
);
}
dispatch({
type: COMPOSE_REMOVE_AVATAR,
payload: avatarData,
});
};
}
function composeReplaceAvatar(
curr: AvatarDataType,
prev?: AvatarDataType
): ComposeReplaceAvatarsActionType {
return {
type: COMPOSE_REPLACE_AVATAR,
payload: {
curr,
prev,
},
};
}
2021-03-03 20:09:58 +00:00
function cantAddContactToGroup(
conversationId: string
): CantAddContactToGroupActionType {
return {
type: 'CANT_ADD_CONTACT_TO_GROUP',
payload: { conversationId },
};
}
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
): ThunkAction<void, RootStateType, unknown, ConversationChangedActionType> {
2022-02-16 18:36:21 +00:00
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,
},
};
}
function conversationUnloaded(id: string): ConversationUnloadedActionType {
return {
type: 'CONVERSATION_UNLOADED',
payload: {
id,
},
};
}
2021-03-03 20:09:58 +00:00
2022-02-11 21:38:52 +00:00
function createGroup(
createGroupV2 = groups.createGroupV2
): ThunkAction<
2021-03-03 20:09:58 +00:00
void,
RootStateType,
unknown,
| CreateGroupPendingActionType
| CreateGroupFulfilledActionType
| CreateGroupRejectedActionType
| SwitchToAssociatedViewActionType
> {
return async (dispatch, getState, ...args) => {
const { composer } = getState().conversations;
if (
composer?.step !== ComposerStep.SetGroupMetadata ||
composer.isCreating
) {
assert(false, 'Cannot create group in this stage; doing nothing');
return;
}
dispatch({ type: 'CREATE_GROUP_PENDING' });
try {
2022-02-11 21:38:52 +00:00
const conversation = await createGroupV2({
name: composer.groupName.trim(),
2021-03-03 20:09:58 +00:00
avatar: composer.groupAvatar,
2021-08-06 00:17:05 +00:00
avatars: composer.userAvatarData.map(avatarData =>
omit(avatarData, ['buffer'])
),
expireTimer: composer.groupExpireTimer,
2021-03-03 20:09:58 +00:00
conversationIds: composer.selectedConversationIds,
});
dispatch({
type: 'CREATE_GROUP_FULFILLED',
payload: {
2021-10-26 22:59:08 +00:00
invitedUuids: (conversation.get('pendingMembersV2') || []).map(
member => member.uuid
),
2021-03-03 20:09:58 +00:00
},
});
openConversationInternal({
conversationId: conversation.id,
switchToAssociatedView: true,
})(dispatch, getState, ...args);
} catch (err) {
log.error('Failed to create group', err && err.stack ? err.stack : err);
2021-03-03 20:09:58 +00:00
dispatch({ type: 'CREATE_GROUP_REJECTED' });
}
};
}
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
function selectMessage(
messageId: string,
conversationId: string
): MessageSelectedActionType {
2019-11-07 21:36:16 +00:00
return {
type: 'MESSAGE_SELECTED',
payload: {
messageId,
conversationId,
},
};
}
2022-02-16 18:36:21 +00:00
function conversationStoppedByMissingVerification(payload: {
conversationId: string;
untrustedConversationIds: ReadonlyArray<string>;
}): ConversationStoppedByMissingVerificationActionType {
return {
2022-02-16 18:36:21 +00:00
type: CONVERSATION_STOPPED_BY_MISSING_VERIFICATION,
payload,
};
}
function messageChanged(
id: string,
conversationId: string,
data: MessageAttributesType
): MessageChangedActionType {
return {
type: 'MESSAGE_CHANGED',
payload: {
id,
conversationId,
data,
},
};
}
function messageDeleted(
2019-01-14 21:49:58 +00:00
id: string,
conversationId: string
): MessageDeletedActionType {
2019-01-14 21:49:58 +00:00
return {
type: 'MESSAGE_DELETED',
2019-01-14 21:49:58 +00:00
payload: {
id,
conversationId,
},
};
}
function messageExpanded(
id: string,
displayLimit: number
): MessageExpandedActionType {
return {
type: 'MESSAGE_EXPANDED',
payload: {
id,
displayLimit,
},
};
}
function messageSizeChanged(
id: string,
conversationId: string
): MessageSizeChangedActionType {
return {
type: 'MESSAGE_SIZE_CHANGED',
payload: {
id,
conversationId,
},
};
}
function messagesAdded({
conversationId,
isActive,
isJustSent,
isNewMessage,
messages,
}: {
conversationId: string;
isActive: boolean;
isJustSent: boolean;
isNewMessage: boolean;
messages: Array<MessageAttributesType>;
}): MessagesAddedActionType {
return {
type: 'MESSAGES_ADDED',
payload: {
conversationId,
isActive,
isJustSent,
isNewMessage,
messages,
},
};
}
function repairNewestMessage(
conversationId: string
): RepairNewestMessageActionType {
return {
type: 'REPAIR_NEWEST_MESSAGE',
payload: {
conversationId,
},
};
}
function repairOldestMessage(
conversationId: string
): RepairOldestMessageActionType {
return {
type: 'REPAIR_OLDEST_MESSAGE',
payload: {
conversationId,
},
};
}
2021-06-01 23:30:25 +00:00
function reviewGroupMemberNameCollision(
groupConversationId: string
): ReviewGroupMemberNameCollisionActionType {
return {
type: 'REVIEW_GROUP_MEMBER_NAME_COLLISION',
payload: { groupConversationId },
};
}
2021-04-21 16:31:12 +00:00
function reviewMessageRequestNameCollision(
payload: Readonly<{
safeConversationId: string;
}>
): ReviewMessageRequestNameCollisionActionType {
return { type: 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION', payload };
}
2022-01-20 00:40:29 +00:00
export type MessageResetOptionsType = Readonly<{
conversationId: string;
messages: Array<MessageAttributesType>;
metrics: MessageMetricsType;
scrollToMessageId?: string;
unboundedFetch?: boolean;
}>;
function messagesReset({
conversationId,
messages,
metrics,
scrollToMessageId,
unboundedFetch,
}: MessageResetOptionsType): MessagesResetActionType {
return {
type: 'MESSAGES_RESET',
payload: {
unboundedFetch: Boolean(unboundedFetch),
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,
},
};
}
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 },
};
}
function setRecentMediaItems(
id: string,
recentMediaItems: Array<MediaItemType>
): SetRecentMediaItemsActionType {
return {
type: 'SET_RECENT_MEDIA_ITEMS',
payload: { id, recentMediaItems },
};
}
function clearChangedMessages(
conversationId: string,
baton: unknown
): ClearChangedMessagesActionType {
return {
type: 'CLEAR_CHANGED_MESSAGES',
payload: {
conversationId,
baton,
},
};
}
2021-10-26 22:59:08 +00:00
function clearInvitedUuidsForNewlyCreatedGroup(): ClearInvitedUuidsForNewlyCreatedGroupActionType {
return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' };
2021-03-03 20:09:58 +00:00
}
function clearGroupCreationError(): ClearGroupCreationErrorActionType {
return { type: 'CLEAR_GROUP_CREATION_ERROR' };
}
function clearSelectedMessage(): ClearSelectedMessageActionType {
return {
type: 'CLEAR_SELECTED_MESSAGE',
payload: null,
};
}
function clearUnreadMetrics(
conversationId: string
): ClearUnreadMetricsActionType {
return {
type: 'CLEAR_UNREAD_METRICS',
payload: {
conversationId,
},
};
}
2021-03-03 20:09:58 +00:00
function closeCantAddContactToGroupModal(): CloseCantAddContactToGroupModalActionType {
return { type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL' };
}
2021-04-21 16:31:12 +00:00
function closeContactSpoofingReview(): CloseContactSpoofingReviewActionType {
return { type: 'CLOSE_CONTACT_SPOOFING_REVIEW' };
}
2021-03-03 20:09:58 +00:00
function closeMaximumGroupSizeModal(): CloseMaximumGroupSizeModalActionType {
return { type: 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL' };
}
function closeRecommendedGroupSizeModal(): CloseRecommendedGroupSizeModalActionType {
return { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' };
}
function scrollToMessage(
conversationId: string,
messageId: string
): ScrollToMessageActionType {
return {
type: 'SCROLL_TO_MESSAGE',
payload: {
conversationId,
messageId,
},
};
}
2019-01-14 21:49:58 +00:00
2021-03-03 20:09:58 +00:00
function setComposeGroupAvatar(
2021-09-24 00:49:05 +00:00
groupAvatar: undefined | Uint8Array
2021-03-03 20:09:58 +00:00
): SetComposeGroupAvatarActionType {
return {
type: 'SET_COMPOSE_GROUP_AVATAR',
payload: { groupAvatar },
};
}
function setComposeGroupName(groupName: string): SetComposeGroupNameActionType {
return {
type: 'SET_COMPOSE_GROUP_NAME',
payload: { groupName },
};
}
function setComposeGroupExpireTimer(
groupExpireTimer: number
): SetComposeGroupExpireTimerActionType {
return {
type: 'SET_COMPOSE_GROUP_EXPIRE_TIMER',
payload: { groupExpireTimer },
};
}
function setComposeSearchTerm(
searchTerm: string
): SetComposeSearchTermActionType {
return {
type: 'SET_COMPOSE_SEARCH_TERM',
payload: { searchTerm },
};
}
function startComposing(): StartComposingActionType {
return { type: 'START_COMPOSING' };
}
2021-03-03 20:09:58 +00:00
function showChooseGroupMembers(): ShowChooseGroupMembersActionType {
return { type: 'SHOW_CHOOSE_GROUP_MEMBERS' };
}
function startNewConversationFromPhoneNumber(
e164: string
): ThunkAction<void, RootStateType, unknown, ShowInboxActionType> {
return dispatch => {
trigger('showConversation', e164);
dispatch(showInbox());
};
}
2021-11-12 01:17:29 +00:00
async function checkForUsername(
username: string
): Promise<FoundUsernameType | undefined> {
if (!isValidUsername(username)) {
return undefined;
}
try {
const profile = await window.textsecure.messaging.getProfileForUsername(
username
);
if (!profile.uuid) {
log.error("checkForUsername: Returned profile didn't include a uuid");
return;
}
return {
uuid: UUID.cast(profile.uuid),
username,
2021-11-12 01:17:29 +00:00
};
} catch (error: unknown) {
if (!isRecord(error)) {
throw error;
}
if (error.code === 404) {
return undefined;
}
throw error;
}
}
function startNewConversationFromUsername(
username: string
): ThunkAction<
void,
RootStateType,
unknown,
| ShowInboxActionType
| SetIsFetchingUsernameActionType
| ShowUsernameNotFoundModalActionType
> {
return async (dispatch, getState) => {
const state = getState();
const byUsername = getConversationsByUsername(state);
const knownConversation = getOwn(byUsername, username);
if (knownConversation && knownConversation.uuid) {
trigger('showConversation', knownConversation.uuid, username);
dispatch(showInbox());
return;
}
dispatch({
type: 'SET_IS_FETCHING_USERNAME',
payload: {
isFetchingUsername: true,
},
});
try {
const foundUsername = await checkForUsername(username);
dispatch({
type: 'SET_IS_FETCHING_USERNAME',
payload: {
isFetchingUsername: false,
},
});
if (!foundUsername) {
dispatch(globalModalActions.showUsernameNotFoundModal(username));
return;
}
trigger(
'showConversation',
foundUsername.uuid,
undefined,
foundUsername.username
);
dispatch(showInbox());
} catch (error) {
log.error(
'startNewConversationFromUsername: Something went wrong fetching username:',
error.stack
);
dispatch({
type: 'SET_IS_FETCHING_USERNAME',
payload: {
isFetchingUsername: false,
},
});
showToast(ToastFailedToFetchUsername);
}
};
}
2021-03-03 20:09:58 +00:00
function startSettingGroupMetadata(): StartSettingGroupMetadataActionType {
return { type: 'START_SETTING_GROUP_METADATA' };
}
function toggleConversationInChooseMembers(
conversationId: string
): ThunkAction<
void,
RootStateType,
unknown,
ToggleConversationInChooseMembersActionType
> {
return dispatch => {
const maxRecommendedGroupSize = getGroupSizeRecommendedLimit(151);
const maxGroupSize = Math.max(
getGroupSizeHardLimit(1001),
maxRecommendedGroupSize + 1
);
assert(
maxGroupSize > maxRecommendedGroupSize,
'Expected the hard max group size to be larger than the recommended maximum'
);
dispatch({
type: 'TOGGLE_CONVERSATION_IN_CHOOSE_MEMBERS',
payload: { conversationId, maxGroupSize, maxRecommendedGroupSize },
});
};
}
2019-01-14 21:49:58 +00:00
// Note: we need two actions here to simplify. Operations outside of the left pane can
// trigger an 'openConversation' so we go through Whisper.events for all
// conversation selection. Internal just triggers the Whisper.event, and External
// makes the changes to the store.
function openConversationInternal({
conversationId,
messageId,
switchToAssociatedView,
}: Readonly<{
conversationId: string;
messageId?: string;
switchToAssociatedView?: boolean;
}>): ThunkAction<
void,
RootStateType,
unknown,
SwitchToAssociatedViewActionType
> {
return dispatch => {
trigger('showConversation', conversationId, messageId);
2019-01-14 21:49:58 +00:00
if (switchToAssociatedView) {
dispatch({
type: 'SWITCH_TO_ASSOCIATED_VIEW',
payload: { conversationId },
});
}
2019-01-14 21:49:58 +00:00
};
}
function openConversationExternal(
id: string,
messageId?: string
): SelectedConversationChangedActionType {
return {
type: 'SELECTED_CONVERSATION_CHANGED',
payload: {
id,
messageId,
},
};
}
2021-09-21 22:37:10 +00:00
function removeMemberFromGroup(
conversationId: string,
contactId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return dispatch => {
const conversationModel = window.ConversationController.get(conversationId);
if (conversationModel) {
const idForLogging = conversationModel.idForLogging();
longRunningTaskWrapper({
name: 'removeMemberFromGroup',
idForLogging,
task: () => conversationModel.removeFromGroupV2(contactId),
});
}
dispatch({
type: 'NOOP',
payload: null,
});
};
}
function toggleAdmin(
conversationId: string,
contactId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return dispatch => {
const conversationModel = window.ConversationController.get(conversationId);
if (conversationModel) {
conversationModel.toggleAdmin(contactId);
}
dispatch({
type: 'NOOP',
payload: null,
});
};
}
function updateConversationModelSharedGroups(
conversationId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (conversation && conversation.throttledUpdateSharedGroups) {
conversation.throttledUpdateSharedGroups();
}
dispatch({
type: 'NOOP',
payload: null,
});
};
}
function showInbox(): ShowInboxActionType {
2019-03-12 00:20:16 +00:00
return {
type: 'SHOW_INBOX',
payload: null,
};
}
function showArchivedConversations(): ShowArchivedConversationsActionType {
2019-03-12 00:20:16 +00:00
return {
type: 'SHOW_ARCHIVED_CONVERSATIONS',
payload: null,
};
}
function doubleCheckMissingQuoteReference(messageId: string): NoopActionType {
const message = window.MessageController.getById(messageId);
if (message) {
message.doubleCheckMissingQuoteReference();
}
return {
type: 'NOOP',
payload: null,
};
}
2019-01-14 21:49:58 +00:00
// Reducer
export function getEmptyState(): ConversationsStateType {
2019-01-14 21:49:58 +00:00
return {
conversationLookup: {},
conversationsByE164: {},
conversationsByUuid: {},
conversationsByGroupId: {},
2021-11-12 01:17:29 +00:00
conversationsByUsername: {},
2022-02-16 18:36:21 +00:00
verificationDataByConversation: {},
messagesByConversation: {},
messagesLookup: {},
selectedMessageCounter: 0,
showArchived: false,
selectedConversationTitle: '',
2020-10-30 17:52:21 +00:00
selectedConversationPanelDepth: 0,
usernameSaveState: UsernameSaveState.None,
2019-01-14 21:49:58 +00:00
};
}
function hasMessageHeightChanged(
message: MessageAttributesType,
previous: MessageAttributesType
): boolean {
const messageAttachments = message.attachments || [];
const previousAttachments = previous.attachments || [];
const errorStatusChanged =
(!message.errors && previous.errors) ||
(message.errors && !previous.errors) ||
(message.errors &&
previous.errors &&
message.errors.length !== previous.errors.length);
if (errorStatusChanged) {
return true;
}
const groupUpdateChanged = message.group_update !== previous.group_update;
if (groupUpdateChanged) {
return true;
}
const stickerPendingChanged =
message.sticker &&
message.sticker.data &&
previous.sticker &&
previous.sticker.data &&
!previous.sticker.data.blurHash &&
previous.sticker.data.pending !== message.sticker.data.pending;
if (stickerPendingChanged) {
return true;
}
const longMessageAttachmentLoaded =
previous.bodyPending && !message.bodyPending;
if (longMessageAttachmentLoaded) {
return true;
}
const firstAttachmentNoLongerPending =
previousAttachments[0] &&
previousAttachments[0].pending &&
messageAttachments[0] &&
!messageAttachments[0].pending;
if (firstAttachmentNoLongerPending) {
return true;
}
2019-08-20 19:34:52 +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;
}
return false;
}
export function updateConversationLookups(
added: ConversationType | undefined,
removed: ConversationType | undefined,
state: ConversationsStateType
): Pick<
ConversationsStateType,
2021-11-12 01:17:29 +00:00
| 'conversationsByE164'
| 'conversationsByUuid'
| 'conversationsByGroupId'
| 'conversationsByUsername'
> {
const result = {
conversationsByE164: state.conversationsByE164,
conversationsByUuid: state.conversationsByUuid,
conversationsByGroupId: state.conversationsByGroupId,
2021-11-12 01:17:29 +00:00
conversationsByUsername: state.conversationsByUsername,
};
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
);
}
2021-11-12 01:17:29 +00:00
if (removed && removed.username) {
result.conversationsByUsername = omit(
result.conversationsByUsername,
removed.username
);
}
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,
};
}
2021-11-12 01:17:29 +00:00
if (added && added.username) {
result.conversationsByUsername = {
...result.conversationsByUsername,
[added.username]: added,
};
}
return result;
}
2021-03-03 20:09:58 +00:00
function closeComposerModal(
state: Readonly<ConversationsStateType>,
modalToClose: 'maximumGroupSizeModalState' | 'recommendedGroupSizeModalState'
): ConversationsStateType {
const { composer } = state;
if (composer?.step !== ComposerStep.ChooseGroupMembers) {
assert(false, "Can't close the modal in this composer step. Doing nothing");
return state;
}
if (composer[modalToClose] !== OneTimeModalState.Showing) {
return state;
}
return {
...state,
composer: {
...composer,
[modalToClose]: OneTimeModalState.Shown,
},
};
}
2019-01-14 21:49:58 +00:00
export function reducer(
state: Readonly<ConversationsStateType> = getEmptyState(),
action: Readonly<ConversationActionType>
2019-01-14 21:49:58 +00:00
): ConversationsStateType {
2022-02-16 18:36:21 +00:00
if (action.type === CLEAR_CONVERSATIONS_PENDING_VERIFICATION) {
return {
...state,
verificationDataByConversation: {},
};
}
if (action.type === CLEAR_CANCELLED_VERIFICATION) {
const { conversationId } = action.payload;
const { verificationDataByConversation } = state;
const existingPendingState = getOwn(
verificationDataByConversation,
conversationId
);
// If there are active verifications required, this will do nothing.
if (
existingPendingState &&
existingPendingState.type ===
ConversationVerificationState.PendingVerification
) {
return state;
}
return {
...state,
verificationDataByConversation: omit(
verificationDataByConversation,
conversationId
),
};
}
if (action.type === CANCEL_CONVERSATION_PENDING_VERIFICATION) {
const { canceledAt } = action.payload;
const { verificationDataByConversation } = state;
const newverificationDataByConversation: Record<
string,
ConversationVerificationData
> = {};
const entries = Object.entries(verificationDataByConversation);
if (!entries.length) {
log.warn(
'CANCEL_CONVERSATION_PENDING_VERIFICATION: No conversations pending verification'
);
return state;
}
for (const [conversationId, data] of entries) {
if (
data.type === ConversationVerificationState.VerificationCancelled &&
data.canceledAt > canceledAt
) {
newverificationDataByConversation[conversationId] = data;
} else {
newverificationDataByConversation[conversationId] = {
type: ConversationVerificationState.VerificationCancelled,
canceledAt,
};
}
}
return {
...state,
2022-02-16 18:36:21 +00:00
verificationDataByConversation: newverificationDataByConversation,
};
}
2021-03-03 20:09:58 +00:00
if (action.type === 'CANT_ADD_CONTACT_TO_GROUP') {
const { composer } = state;
if (composer?.step !== ComposerStep.ChooseGroupMembers) {
assert(false, "Can't update modal in this composer step. Doing nothing");
return state;
}
return {
...state,
composer: {
...composer,
cantAddContactIdForModal: action.payload.conversationId,
},
};
}
2021-10-26 22:59:08 +00:00
if (action.type === 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP') {
return omit(state, 'invitedUuidsForNewlyCreatedGroup');
2021-03-03 20:09:58 +00:00
}
if (action.type === 'CLEAR_GROUP_CREATION_ERROR') {
const { composer } = state;
if (composer?.step !== ComposerStep.SetGroupMetadata) {
assert(
false,
"Can't clear group creation error in this composer state. Doing nothing"
);
return state;
}
return {
...state,
composer: {
...composer,
hasError: false,
},
};
}
if (action.type === 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL') {
const { composer } = state;
if (composer?.step !== ComposerStep.ChooseGroupMembers) {
assert(
false,
"Can't close the modal in this composer step. Doing nothing"
);
return state;
}
return {
...state,
composer: {
...composer,
cantAddContactIdForModal: undefined,
},
};
}
2021-04-21 16:31:12 +00:00
if (action.type === 'CLOSE_CONTACT_SPOOFING_REVIEW') {
return omit(state, 'contactSpoofingReview');
}
2021-03-03 20:09:58 +00:00
if (action.type === 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL') {
return closeComposerModal(state, 'maximumGroupSizeModalState' as const);
}
if (action.type === 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL') {
return closeComposerModal(state, 'recommendedGroupSizeModalState' as const);
}
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,
},
...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;
2021-04-21 16:31:12 +00:00
const { selectedConversationId } = state;
let { showArchived } = state;
2019-03-12 00:20:16 +00:00
const existing = conversationLookup[id];
// We only modify the lookup if we already had that conversation and the conversation
// changed.
if (!existing || data === existing) {
2019-01-14 21:49:58 +00:00
return state;
}
2021-04-21 16:31:12 +00:00
const keysToOmit: Array<keyof ConversationsStateType> = [];
if (selectedConversationId === id) {
2019-03-12 00:20:16 +00:00
// Archived -> Inbox: we go back to the normal inbox view
if (existing.isArchived && !data.isArchived) {
showArchived = false;
}
// Inbox -> Archived: no conversation is selected
2022-02-16 18:36:21 +00:00
// Note: With today's stacked conversations architecture, this can result in weird
2019-03-12 00:20:16 +00:00
// behavior - no selected conversation in the left pane, but a conversation show
// in the right pane.
if (!existing.isArchived && data.isArchived) {
2021-04-21 16:31:12 +00:00
keysToOmit.push('selectedConversationId');
}
if (!existing.isBlocked && data.isBlocked) {
keysToOmit.push('contactSpoofingReview');
2019-03-12 00:20:16 +00:00
}
}
2019-01-14 21:49:58 +00:00
return {
2021-04-21 16:31:12 +00:00
...omit(state, keysToOmit),
selectedConversationId,
2019-03-12 00:20:16 +00:00
showArchived,
2019-01-14 21:49:58 +00:00
conversationLookup: {
...conversationLookup,
[id]: data,
},
...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;
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]),
...updateConversationLookups(undefined, existing, state),
2019-01-14 21:49:58 +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;
const selectedConversationId =
state.selectedConversationId !== id
? state.selectedConversationId
2019-11-07 21:36:16 +00:00
: undefined;
return {
2021-04-21 16:31:12 +00:00
...omit(state, 'contactSpoofingReview'),
selectedConversationId,
2020-10-30 17:52:21 +00:00
selectedConversationPanelDepth: 0,
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();
}
2021-03-03 20:09:58 +00:00
if (action.type === 'CREATE_GROUP_PENDING') {
const { composer } = state;
if (composer?.step !== ComposerStep.SetGroupMetadata) {
// This should be unlikely, but it can happen if someone closes the composer while
// a group is being created.
return state;
}
return {
...state,
composer: {
...composer,
hasError: false,
isCreating: true,
},
};
}
if (action.type === 'CREATE_GROUP_FULFILLED') {
// We don't do much here and instead rely on `openConversationInternal` to do most of
// the work.
return {
...state,
2021-10-26 22:59:08 +00:00
invitedUuidsForNewlyCreatedGroup: action.payload.invitedUuids,
2021-03-03 20:09:58 +00:00
};
}
if (action.type === 'CREATE_GROUP_REJECTED') {
const { composer } = state;
if (composer?.step !== ComposerStep.SetGroupMetadata) {
// This should be unlikely, but it can happen if someone closes the composer while
// a group is being created.
return state;
}
return {
...state,
composer: {
...composer,
hasError: true,
isCreating: false,
},
};
}
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.selectedConversationId !== conversationId) {
2019-11-07 21:36:16 +00:00
return state;
}
return {
...state,
selectedMessage: messageId,
selectedMessageCounter: state.selectedMessageCounter + 1,
};
}
2022-02-16 18:36:21 +00:00
if (action.type === CONVERSATION_STOPPED_BY_MISSING_VERIFICATION) {
const { conversationId, untrustedConversationIds } = action.payload;
2022-02-16 18:36:21 +00:00
const { verificationDataByConversation } = state;
const existingPendingState = getOwn(
verificationDataByConversation,
conversationId
);
if (
!existingPendingState ||
existingPendingState.type ===
ConversationVerificationState.VerificationCancelled
) {
return {
...state,
verificationDataByConversation: {
...verificationDataByConversation,
[conversationId]: {
type: ConversationVerificationState.PendingVerification as const,
conversationsNeedingVerification: untrustedConversationIds,
},
},
};
}
const conversationsNeedingVerification: ReadonlyArray<string> = Array.from(
new Set([
...existingPendingState.conversationsNeedingVerification,
...untrustedConversationIds,
])
);
return {
...state,
2022-02-16 18:36:21 +00:00
verificationDataByConversation: {
...verificationDataByConversation,
[conversationId]: {
type: ConversationVerificationState.PendingVerification as const,
conversationsNeedingVerification,
},
},
};
}
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 = getOwn(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,
displayLimit: existingMessage.displayLimit,
},
},
messagesByConversation: {
...state.messagesByConversation,
[conversationId]: {
...existingConversation,
heightChangeMessageIds: updatedChanges,
},
},
};
}
if (action.type === 'MESSAGE_EXPANDED') {
const { id, displayLimit } = action.payload;
const existingMessage = state.messagesLookup[id];
if (!existingMessage) {
return state;
}
return {
...state,
messagesLookup: {
...state.messagesLookup,
[id]: {
...existingMessage,
displayLimit,
},
},
};
}
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,
]),
},
},
};
}
if (action.type === 'MESSAGES_RESET') {
const {
conversationId,
messages,
metrics,
scrollToMessageId,
unboundedFetch,
} = action.payload;
const { messagesByConversation, messagesLookup } = state;
const existingConversation = messagesByConversation[conversationId];
const resetCounter = existingConversation
? existingConversation.resetCounter + 1
: 0;
const lookup = fromPairs(messages.map(message => [message.id, message]));
const sorted = orderBy(
values(lookup),
['received_at', 'sent_at'],
['ASC', 'ASC']
);
let { newest, oldest } = metrics;
// If our metrics are a little out of date, we'll fix them up
if (sorted.length > 0) {
const first = sorted[0];
if (first && (!oldest || first.received_at <= oldest.received_at)) {
oldest = pick(first, ['id', 'received_at', 'sent_at']);
}
const last = sorted[sorted.length - 1];
if (
last &&
(!newest || unboundedFetch || last.received_at >= newest.received_at)
) {
newest = pick(last, ['id', 'received_at', 'sent_at']);
}
}
2021-09-28 18:32:54 +00:00
const messageIds = sorted.map(message => message.id);
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,
messageIds,
metrics: {
...metrics,
newest,
oldest,
},
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]];
oldest = second
? pick(second, ['id', 'received_at', 'sent_at'])
: undefined;
}
if (newest && newest.id === lastId && lastId === id) {
const penultimate = messagesLookup[oldIds[oldIds.length - 2]];
newest = penultimate
? pick(penultimate, ['id', 'received_at', 'sent_at'])
: 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,
};
}
return {
...state,
messagesLookup: omit(messagesLookup, id),
messagesByConversation: {
[conversationId]: {
...existingConversation,
messageIds,
heightChangeMessageIds,
2020-09-09 02:25:05 +00:00
metrics,
},
},
};
}
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;
const newest = last
? pick(last, ['id', 'received_at', 'sent_at'])
: undefined;
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;
const oldest = first
? pick(first, ['id', 'received_at', 'sent_at'])
: undefined;
return {
...state,
messagesByConversation: {
...messagesByConversation,
[conversationId]: {
...existingConversation,
metrics: {
...existingConversation.metrics,
oldest,
},
},
},
};
}
2021-06-01 23:30:25 +00:00
if (action.type === 'REVIEW_GROUP_MEMBER_NAME_COLLISION') {
return {
...state,
contactSpoofingReview: {
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
...action.payload,
},
};
}
2021-04-21 16:31:12 +00:00
if (action.type === 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION') {
return {
...state,
2021-06-01 23:30:25 +00:00
contactSpoofingReview: {
type: ContactSpoofingType.DirectConversationWithSameTitle,
...action.payload,
},
2021-04-21 16:31:12 +00:00
};
}
if (action.type === 'MESSAGES_ADDED') {
const { conversationId, isActive, isJustSent, isNewMessage, messages } =
action.payload;
const { messagesByConversation, messagesLookup } = state;
const existingConversation = messagesByConversation[conversationId];
if (!existingConversation) {
return state;
}
2021-11-11 22:43:05 +00:00
let { newest, oldest, oldestUnread, totalUnread } =
existingConversation.metrics;
2019-08-15 14:59:56 +00:00
if (messages.length < 1) {
return state;
}
const lookup = fromPairs(
existingConversation.messageIds.map(id => [id, messagesLookup[id]])
);
messages.forEach(message => {
lookup[message.id] = message;
});
const sorted = orderBy(
values(lookup),
['received_at', 'sent_at'],
['ASC', 'ASC']
);
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) {
newest = pick(first, ['id', 'received_at', 'sent_at']);
2019-08-15 14:59:56 +00:00
}
if (!oldest) {
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) {
if (isJustSent) {
log.warn(
'reducer/MESSAGES_ADDED: isJustSent is true, but haveLatest is false'
);
}
2019-08-15 14:59:56 +00:00
return state;
}
}
// Update oldest and newest if we receive older/newer
// messages (or duplicated timestamps!)
if (first && oldest && first.received_at <= oldest.received_at) {
oldest = pick(first, ['id', 'received_at', 'sent_at']);
}
if (last && newest && last.received_at >= newest.received_at) {
newest = pick(last, ['id', 'received_at', 'sent_at']);
}
2019-08-15 14:59:56 +00:00
const newIds = messages.map(message => message.id);
const newMessageIds = difference(newIds, existingConversation.messageIds);
const { isNearBottom } = existingConversation;
if ((!isNearBottom || !isActive) && !oldestUnread) {
const oldestId = newMessageIds.find(messageId => {
const message = lookup[messageId];
return message && isMessageUnread(message);
});
if (oldestId) {
oldestUnread = pick(lookup[oldestId], [
'id',
'received_at',
'sent_at',
]) as MessagePointerType;
}
}
if (oldestUnread) {
const newUnread: number = newMessageIds.reduce((sum, messageId) => {
const message = lookup[messageId];
return sum + (message && isMessageUnread(message) ? 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,
]);
return {
...state,
messagesLookup: {
...messagesLookup,
...lookup,
},
messagesByConversation: {
...messagesByConversation,
[conversationId]: {
...existingConversation,
isLoadingMessages: false,
messageIds,
heightChangeMessageIds,
scrollToMessageId: isJustSent ? last.id : 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, baton } = payload;
const existingConversation = state.messagesByConversation[conversationId];
if (
!existingConversation ||
existingConversation.heightChangeMessageIds !== baton
) {
log.warn('CLEAR_CHANGED_MESSAGES used expired baton');
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 {
2021-04-21 16:31:12 +00:00
...omit(state, 'contactSpoofingReview'),
selectedConversationId: id,
2019-03-12 00:20:16 +00:00
};
}
if (action.type === 'SHOW_INBOX') {
return {
...omit(state, 'composer'),
2019-03-12 00:20:16 +00:00
showArchived: false,
};
}
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
return {
...omit(state, 'composer'),
2019-03-12 00:20:16 +00:00
showArchived: true,
};
}
2019-01-14 21:49:58 +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),
};
}
if (action.type === 'START_COMPOSING') {
2021-03-03 20:09:58 +00:00
if (state.composer?.step === ComposerStep.StartDirectConversation) {
return state;
}
return {
...state,
showArchived: false,
composer: {
2021-03-03 20:09:58 +00:00
step: ComposerStep.StartDirectConversation,
searchTerm: '',
2021-11-12 01:17:29 +00:00
isFetchingUsername: false,
2021-03-03 20:09:58 +00:00
},
};
}
if (action.type === 'SHOW_CHOOSE_GROUP_MEMBERS') {
let selectedConversationIds: Array<string>;
let recommendedGroupSizeModalState: OneTimeModalState;
let maximumGroupSizeModalState: OneTimeModalState;
let groupName: string;
2021-09-24 00:49:05 +00:00
let groupAvatar: undefined | Uint8Array;
let groupExpireTimer: number;
2021-08-06 00:17:05 +00:00
let userAvatarData = getDefaultAvatars(true);
2021-03-03 20:09:58 +00:00
switch (state.composer?.step) {
case ComposerStep.ChooseGroupMembers:
return state;
case ComposerStep.SetGroupMetadata:
({
selectedConversationIds,
recommendedGroupSizeModalState,
maximumGroupSizeModalState,
groupName,
groupAvatar,
groupExpireTimer,
2021-08-06 00:17:05 +00:00
userAvatarData,
2021-03-03 20:09:58 +00:00
} = state.composer);
break;
default:
selectedConversationIds = [];
recommendedGroupSizeModalState = OneTimeModalState.NeverShown;
maximumGroupSizeModalState = OneTimeModalState.NeverShown;
groupName = '';
groupExpireTimer = universalExpireTimer.get();
2021-03-03 20:09:58 +00:00
break;
}
return {
...state,
showArchived: false,
composer: {
step: ComposerStep.ChooseGroupMembers,
searchTerm: '',
2021-03-03 20:09:58 +00:00
selectedConversationIds,
cantAddContactIdForModal: undefined,
recommendedGroupSizeModalState,
maximumGroupSizeModalState,
groupName,
groupAvatar,
groupExpireTimer,
2021-08-06 00:17:05 +00:00
userAvatarData,
},
};
}
2021-03-03 20:09:58 +00:00
if (action.type === 'START_SETTING_GROUP_METADATA') {
const { composer } = state;
switch (composer?.step) {
case ComposerStep.ChooseGroupMembers:
return {
...state,
showArchived: false,
composer: {
step: ComposerStep.SetGroupMetadata,
isEditingAvatar: false,
2021-03-03 20:09:58 +00:00
isCreating: false,
hasError: false,
...pick(composer, [
'groupAvatar',
'groupName',
'groupExpireTimer',
2021-03-03 20:09:58 +00:00
'maximumGroupSizeModalState',
'recommendedGroupSizeModalState',
'selectedConversationIds',
2021-08-06 00:17:05 +00:00
'userAvatarData',
2021-03-03 20:09:58 +00:00
]),
},
};
case ComposerStep.SetGroupMetadata:
return state;
default:
assert(
false,
'Cannot transition to setting group metadata from this state'
);
return state;
}
}
if (action.type === 'SET_COMPOSE_GROUP_AVATAR') {
const { composer } = state;
switch (composer?.step) {
case ComposerStep.ChooseGroupMembers:
case ComposerStep.SetGroupMetadata:
return {
...state,
composer: {
...composer,
groupAvatar: action.payload.groupAvatar,
},
};
default:
assert(false, 'Setting compose group avatar at this step is a no-op');
return state;
}
}
if (action.type === 'SET_COMPOSE_GROUP_NAME') {
const { composer } = state;
switch (composer?.step) {
case ComposerStep.ChooseGroupMembers:
case ComposerStep.SetGroupMetadata:
return {
...state,
composer: {
...composer,
groupName: action.payload.groupName,
},
};
default:
assert(false, 'Setting compose group name at this step is a no-op');
return state;
}
}
if (action.type === 'SET_COMPOSE_GROUP_EXPIRE_TIMER') {
const { composer } = state;
switch (composer?.step) {
case ComposerStep.ChooseGroupMembers:
case ComposerStep.SetGroupMetadata:
return {
...state,
composer: {
...composer,
groupExpireTimer: action.payload.groupExpireTimer,
},
};
default:
assert(false, 'Setting compose group name at this step is a no-op');
return state;
}
}
if (action.type === 'SET_COMPOSE_SEARCH_TERM') {
const { composer } = state;
if (!composer) {
assert(
false,
'Setting compose search term with the composer closed is a no-op'
);
return state;
}
2021-11-12 01:17:29 +00:00
if (
composer.step !== ComposerStep.StartDirectConversation &&
composer.step !== ComposerStep.ChooseGroupMembers
) {
assert(
false,
`Setting compose search term at step ${composer.step} is a no-op`
);
2021-03-03 20:09:58 +00:00
return state;
}
return {
...state,
composer: {
...composer,
searchTerm: action.payload.searchTerm,
},
};
}
2021-11-12 01:17:29 +00:00
if (action.type === 'SET_IS_FETCHING_USERNAME') {
const { composer } = state;
if (!composer) {
assert(
false,
'Setting compose username with the composer closed is a no-op'
);
return state;
}
if (composer.step !== ComposerStep.StartDirectConversation) {
assert(false, 'Setting compose username at this step is a no-op');
return state;
}
const { isFetchingUsername } = action.payload;
return {
...state,
composer: {
...composer,
isFetchingUsername,
},
};
}
2021-08-06 00:17:05 +00:00
if (action.type === COMPOSE_TOGGLE_EDITING_AVATAR) {
const { composer } = state;
switch (composer?.step) {
case ComposerStep.SetGroupMetadata:
return {
...state,
composer: {
...composer,
isEditingAvatar: !composer.isEditingAvatar,
2021-08-06 00:17:05 +00:00
},
};
default:
assert(false, 'Setting editing avatar at this step is a no-op');
return state;
}
}
if (action.type === COMPOSE_ADD_AVATAR) {
const { payload } = action;
const { composer } = state;
switch (composer?.step) {
case ComposerStep.ChooseGroupMembers:
case ComposerStep.SetGroupMetadata:
return {
...state,
composer: {
...composer,
userAvatarData: [
{
...payload,
id: getNextAvatarId(composer.userAvatarData),
},
...composer.userAvatarData,
],
},
};
default:
assert(false, 'Adding an avatar at this step is a no-op');
return state;
}
}
if (action.type === COMPOSE_REMOVE_AVATAR) {
const { payload } = action;
const { composer } = state;
switch (composer?.step) {
case ComposerStep.ChooseGroupMembers:
case ComposerStep.SetGroupMetadata:
return {
...state,
composer: {
...composer,
userAvatarData: filterAvatarData(composer.userAvatarData, payload),
},
};
default:
assert(false, 'Removing an avatar at this step is a no-op');
return state;
}
}
if (action.type === COMPOSE_REPLACE_AVATAR) {
const { curr, prev } = action.payload;
const { composer } = state;
switch (composer?.step) {
case ComposerStep.ChooseGroupMembers:
case ComposerStep.SetGroupMetadata:
return {
...state,
composer: {
...composer,
userAvatarData: [
{
...curr,
id: prev?.id ?? getNextAvatarId(composer.userAvatarData),
},
...(prev
? filterAvatarData(composer.userAvatarData, prev)
: composer.userAvatarData),
],
},
};
default:
assert(false, 'Replacing an avatar at this step is a no-op');
return state;
}
}
if (action.type === 'SWITCH_TO_ASSOCIATED_VIEW') {
const conversation = getOwn(
state.conversationLookup,
action.payload.conversationId
);
if (!conversation) {
return state;
}
return {
...omit(state, 'composer'),
showArchived: Boolean(conversation.isArchived),
};
}
2021-03-03 20:09:58 +00:00
if (action.type === 'TOGGLE_CONVERSATION_IN_CHOOSE_MEMBERS') {
const { composer } = state;
if (composer?.step !== ComposerStep.ChooseGroupMembers) {
assert(
false,
'Toggling conversation members is a no-op in this composer step'
);
return state;
}
return {
...state,
composer: {
...composer,
2021-03-11 21:29:31 +00:00
...toggleSelectedContactForGroupAddition(
action.payload.conversationId,
{
maxGroupSize: action.payload.maxGroupSize,
maxRecommendedGroupSize: action.payload.maxRecommendedGroupSize,
maximumGroupSizeModalState: composer.maximumGroupSizeModalState,
// We say you're already in the group, even though it hasn't been created yet.
numberOfContactsAlreadyInGroup: 1,
recommendedGroupSizeModalState:
composer.recommendedGroupSizeModalState,
selectedConversationIds: composer.selectedConversationIds,
}
),
2021-03-03 20:09:58 +00:00
},
};
}
2021-05-28 16:15:17 +00:00
if (action.type === COLORS_CHANGED) {
const { conversationLookup } = state;
const { conversationColor, customColorData } = action.payload;
const nextState = {
...state,
};
Object.keys(conversationLookup).forEach(id => {
const existing = conversationLookup[id];
const added = {
...existing,
conversationColor,
customColor: customColorData?.value,
customColorId: customColorData?.id,
};
Object.assign(
nextState,
updateConversationLookups(added, existing, nextState),
{
conversationLookup: {
...nextState.conversationLookup,
[id]: added,
},
}
);
});
return nextState;
}
2021-06-03 21:34:36 +00:00
if (action.type === COLOR_SELECTED) {
const { conversationLookup } = state;
2021-11-11 22:43:05 +00:00
const { conversationId, conversationColor, customColorData } =
action.payload;
2021-06-03 21:34:36 +00:00
const existing = conversationLookup[conversationId];
if (!existing) {
return state;
}
const changed = {
...existing,
conversationColor,
customColor: customColorData?.value,
customColorId: customColorData?.id,
};
return {
...state,
conversationLookup: {
...conversationLookup,
[conversationId]: changed,
},
...updateConversationLookups(changed, existing, state),
};
}
2021-05-28 16:15:17 +00:00
if (action.type === CUSTOM_COLOR_REMOVED) {
const { conversationLookup } = state;
const { colorId } = action.payload;
2021-05-28 16:15:17 +00:00
const nextState = {
...state,
};
Object.keys(conversationLookup).forEach(id => {
const existing = conversationLookup[id];
if (existing.customColorId !== colorId) {
return;
}
2021-06-02 21:05:09 +00:00
const changed = {
...existing,
conversationColor: undefined,
customColor: undefined,
customColorId: undefined,
2021-06-02 21:05:09 +00:00
};
2021-05-28 16:15:17 +00:00
Object.assign(
nextState,
updateConversationLookups(changed, existing, nextState),
{
conversationLookup: {
...nextState.conversationLookup,
[id]: changed,
},
}
);
});
return nextState;
}
2021-08-06 00:17:05 +00:00
if (action.type === REPLACE_AVATARS) {
const { conversationLookup } = state;
const { conversationId, avatars } = action.payload;
const conversation = conversationLookup[conversationId];
if (!conversation) {
return state;
}
const changed = {
...conversation,
avatars,
};
return {
...state,
conversationLookup: {
...conversationLookup,
[conversationId]: changed,
},
...updateConversationLookups(changed, conversation, state),
};
}
if (action.type === UPDATE_USERNAME_SAVE_STATE) {
const { newSaveState } = action.payload;
return {
...state,
usernameSaveState: newSaveState,
};
}
2019-01-14 21:49:58 +00:00
return state;
}