2021-02-23 20:34:28 +00:00
// Copyright 2020-2021 Signal Messenger, LLC
2020-12-04 20:41:40 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai' ;
2021-02-23 20:34:28 +00:00
import * as sinon from 'sinon' ;
2021-03-03 20:09:58 +00:00
import { v4 as uuid } from 'uuid' ;
import { times } from 'lodash' ;
2020-12-07 20:43:19 +00:00
import { set } from 'lodash/fp' ;
2021-02-23 20:34:28 +00:00
import { reducer as rootReducer } from '../../../state/reducer' ;
import { noopAction } from '../../../state/ducks/noop' ;
2021-11-01 19:13:35 +00:00
import {
OneTimeModalState ,
ComposerStep ,
} from '../../../state/ducks/conversationsEnums' ;
2021-10-26 19:15:33 +00:00
import type {
2020-12-04 20:41:40 +00:00
ConversationMessageType ,
ConversationType ,
2021-03-03 20:09:58 +00:00
ConversationsStateType ,
MessageType ,
SwitchToAssociatedViewActionType ,
ToggleConversationInChooseMembersActionType ,
2021-10-26 19:15:33 +00:00
} from '../../../state/ducks/conversations' ;
import {
actions ,
2020-12-04 20:41:40 +00:00
getConversationCallMode ,
2021-01-06 15:41:43 +00:00
getEmptyState ,
2020-12-04 20:41:40 +00:00
reducer ,
2021-01-06 15:41:43 +00:00
updateConversationLookups ,
2020-12-04 20:41:40 +00:00
} from '../../../state/ducks/conversations' ;
2021-08-12 18:15:55 +00:00
import { ReadStatus } from '../../../messages/MessageReadStatus' ;
2021-06-01 23:30:25 +00:00
import { ContactSpoofingType } from '../../../util/contactSpoofing' ;
2020-12-04 20:41:40 +00:00
import { CallMode } from '../../../types/Calling' ;
2021-10-26 22:59:08 +00:00
import { UUID } from '../../../types/UUID' ;
2021-03-03 20:09:58 +00:00
import * as groups from '../../../groups' ;
2021-10-26 22:59:08 +00:00
import {
getDefaultConversation ,
getDefaultConversationWithUuid ,
} from '../../../test-both/helpers/getDefaultConversation' ;
2021-08-06 00:17:05 +00:00
import { getDefaultAvatars } from '../../../types/Avatar' ;
2021-08-16 14:33:27 +00:00
import {
defaultStartDirectConversationComposerState ,
defaultChooseGroupMembersComposerState ,
defaultSetGroupMetadataComposerState ,
} from '../../../test-both/helpers/defaultComposerStates' ;
2020-12-04 20:41:40 +00:00
2020-12-07 20:43:19 +00:00
const {
2021-03-03 20:09:58 +00:00
cantAddContactToGroup ,
clearGroupCreationError ,
2021-10-26 22:59:08 +00:00
clearInvitedUuidsForNewlyCreatedGroup ,
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 ,
closeRecommendedGroupSizeModal ,
createGroup ,
2020-12-07 20:43:19 +00:00
messageSizeChanged ,
2021-08-31 20:58:39 +00:00
messageStoppedByMissingVerification ,
2021-02-23 20:34:28 +00:00
openConversationInternal ,
2020-12-07 20:43:19 +00:00
repairNewestMessage ,
repairOldestMessage ,
2021-03-03 20:09:58 +00:00
setComposeGroupAvatar ,
setComposeGroupName ,
2021-02-23 20:34:28 +00:00
setComposeSearchTerm ,
2021-01-29 22:16:48 +00:00
setPreJoinConversation ,
2021-02-23 20:34:28 +00:00
showArchivedConversations ,
showInbox ,
startComposing ,
2021-03-03 20:09:58 +00:00
showChooseGroupMembers ,
startSettingGroupMetadata ,
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-03-03 20:09:58 +00:00
toggleConversationInChooseMembers ,
2020-12-07 20:43:19 +00:00
} = actions ;
2020-12-04 20:41:40 +00:00
describe ( 'both/state/ducks/conversations' , ( ) = > {
2021-02-23 20:34:28 +00:00
const getEmptyRootState = ( ) = > rootReducer ( undefined , noopAction ( ) ) ;
let sinonSandbox : sinon.SinonSandbox ;
2021-03-03 20:09:58 +00:00
let createGroupStub : sinon.SinonStub ;
2021-02-23 20:34:28 +00:00
beforeEach ( ( ) = > {
sinonSandbox = sinon . createSandbox ( ) ;
2021-03-03 20:09:58 +00:00
sinonSandbox . stub ( window . Whisper . events , 'trigger' ) ;
createGroupStub = sinonSandbox . stub ( groups , 'createGroupV2' ) ;
2021-02-23 20:34:28 +00:00
} ) ;
afterEach ( ( ) = > {
sinonSandbox . restore ( ) ;
} ) ;
2020-12-04 20:41:40 +00:00
describe ( 'helpers' , ( ) = > {
describe ( 'getConversationCallMode' , ( ) = > {
2021-05-07 22:21:10 +00:00
const fakeConversation : ConversationType = getDefaultConversation ( ) ;
2020-12-04 20:41:40 +00:00
it ( "returns CallMode.None if you've left the conversation" , ( ) = > {
assert . strictEqual (
getConversationCallMode ( {
. . . fakeConversation ,
left : true ,
} ) ,
CallMode . None
) ;
} ) ;
it ( "returns CallMode.None if you've blocked the other person" , ( ) = > {
assert . strictEqual (
getConversationCallMode ( {
. . . fakeConversation ,
isBlocked : true ,
} ) ,
CallMode . None
) ;
} ) ;
it ( "returns CallMode.None if you haven't accepted message requests" , ( ) = > {
assert . strictEqual (
getConversationCallMode ( {
. . . fakeConversation ,
acceptedMessageRequest : false ,
} ) ,
CallMode . None
) ;
} ) ;
it ( 'returns CallMode.None if the conversation is Note to Self' , ( ) = > {
assert . strictEqual (
getConversationCallMode ( {
. . . fakeConversation ,
isMe : true ,
} ) ,
CallMode . None
) ;
} ) ;
it ( 'returns CallMode.None for v1 groups' , ( ) = > {
assert . strictEqual (
getConversationCallMode ( {
. . . fakeConversation ,
type : 'group' ,
groupVersion : 1 ,
2021-05-07 22:21:10 +00:00
sharedGroupNames : [ ] ,
2020-12-04 20:41:40 +00:00
} ) ,
CallMode . None
) ;
assert . strictEqual (
getConversationCallMode ( {
. . . fakeConversation ,
type : 'group' ,
2021-05-07 22:21:10 +00:00
sharedGroupNames : [ ] ,
2020-12-04 20:41:40 +00:00
} ) ,
CallMode . None
) ;
} ) ;
it ( 'returns CallMode.Direct if the conversation is a normal direct conversation' , ( ) = > {
assert . strictEqual (
getConversationCallMode ( fakeConversation ) ,
CallMode . Direct
) ;
} ) ;
it ( 'returns CallMode.Group if the conversation is a v2 group' , ( ) = > {
assert . strictEqual (
getConversationCallMode ( {
. . . fakeConversation ,
type : 'group' ,
groupVersion : 2 ,
2021-05-07 22:21:10 +00:00
sharedGroupNames : [ ] ,
2020-12-04 20:41:40 +00:00
} ) ,
CallMode . Group
) ;
} ) ;
} ) ;
2021-01-06 15:41:43 +00:00
describe ( 'updateConversationLookups' , ( ) = > {
it ( 'does not change lookups if no conversations provided' , ( ) = > {
const state = getEmptyState ( ) ;
const result = updateConversationLookups ( undefined , undefined , state ) ;
assert . strictEqual (
state . conversationsByE164 ,
result . conversationsByE164
) ;
assert . strictEqual (
state . conversationsByUuid ,
result . conversationsByUuid
) ;
assert . strictEqual (
state . conversationsByGroupId ,
result . conversationsByGroupId
) ;
} ) ;
it ( 'adds and removes e164-only contact' , ( ) = > {
2021-05-07 22:21:10 +00:00
const removed = getDefaultConversation ( {
id : 'id-removed' ,
2021-01-06 15:41:43 +00:00
e164 : 'e164-removed' ,
2021-05-07 22:21:10 +00:00
uuid : undefined ,
} ) ;
2021-01-06 15:41:43 +00:00
const state = {
. . . getEmptyState ( ) ,
conversationsByE164 : {
2021-05-07 22:21:10 +00:00
'e164-removed' : removed ,
2021-01-06 15:41:43 +00:00
} ,
} ;
2021-05-07 22:21:10 +00:00
const added = getDefaultConversation ( {
id : 'id-added' ,
2021-01-06 15:41:43 +00:00
e164 : 'e164-added' ,
2021-05-07 22:21:10 +00:00
uuid : undefined ,
} ) ;
2021-01-06 15:41:43 +00:00
const expected = {
2021-05-07 22:21:10 +00:00
'e164-added' : added ,
2021-01-06 15:41:43 +00:00
} ;
const actual = updateConversationLookups ( added , removed , state ) ;
assert . deepEqual ( actual . conversationsByE164 , expected ) ;
assert . strictEqual (
state . conversationsByUuid ,
actual . conversationsByUuid
) ;
assert . strictEqual (
state . conversationsByGroupId ,
actual . conversationsByGroupId
) ;
} ) ;
it ( 'adds and removes uuid-only contact' , ( ) = > {
2021-10-26 22:59:08 +00:00
const removed = getDefaultConversationWithUuid ( {
2021-05-07 22:21:10 +00:00
id : 'id-removed' ,
e164 : undefined ,
} ) ;
2021-01-06 15:41:43 +00:00
const state = {
. . . getEmptyState ( ) ,
conversationsByuuid : {
2021-10-26 22:59:08 +00:00
[ removed . uuid ] : removed ,
2021-01-06 15:41:43 +00:00
} ,
} ;
2021-10-26 22:59:08 +00:00
const added = getDefaultConversationWithUuid ( {
2021-05-07 22:21:10 +00:00
id : 'id-added' ,
e164 : undefined ,
} ) ;
2021-01-06 15:41:43 +00:00
const expected = {
2021-10-26 22:59:08 +00:00
[ added . uuid ] : added ,
2021-01-06 15:41:43 +00:00
} ;
const actual = updateConversationLookups ( added , removed , state ) ;
assert . strictEqual (
state . conversationsByE164 ,
actual . conversationsByE164
) ;
assert . deepEqual ( actual . conversationsByUuid , expected ) ;
assert . strictEqual (
state . conversationsByGroupId ,
actual . conversationsByGroupId
) ;
} ) ;
it ( 'adds and removes groupId-only contact' , ( ) = > {
2021-05-07 22:21:10 +00:00
const removed = getDefaultConversation ( {
id : 'id-removed' ,
2021-01-06 15:41:43 +00:00
groupId : 'groupId-removed' ,
2021-05-07 22:21:10 +00:00
e164 : undefined ,
uuid : undefined ,
} ) ;
2021-01-06 15:41:43 +00:00
const state = {
. . . getEmptyState ( ) ,
conversationsBygroupId : {
2021-05-07 22:21:10 +00:00
'groupId-removed' : removed ,
2021-01-06 15:41:43 +00:00
} ,
} ;
2021-05-07 22:21:10 +00:00
const added = getDefaultConversation ( {
id : 'id-added' ,
2021-01-06 15:41:43 +00:00
groupId : 'groupId-added' ,
2021-05-07 22:21:10 +00:00
e164 : undefined ,
uuid : undefined ,
} ) ;
2021-01-06 15:41:43 +00:00
const expected = {
2021-05-07 22:21:10 +00:00
'groupId-added' : added ,
2021-01-06 15:41:43 +00:00
} ;
const actual = updateConversationLookups ( added , removed , state ) ;
assert . strictEqual (
state . conversationsByE164 ,
actual . conversationsByE164
) ;
assert . strictEqual (
state . conversationsByUuid ,
actual . conversationsByUuid
) ;
assert . deepEqual ( actual . conversationsByGroupId , expected ) ;
} ) ;
} ) ;
2020-12-04 20:41:40 +00:00
} ) ;
describe ( 'reducer' , ( ) = > {
const time = Date . now ( ) ;
2021-06-17 17:15:10 +00:00
const previousTime = time - 1 ;
2020-12-04 20:41:40 +00:00
const conversationId = 'conversation-guid-1' ;
const messageId = 'message-guid-1' ;
const messageIdTwo = 'message-guid-2' ;
const messageIdThree = 'message-guid-3' ;
2021-10-26 22:59:08 +00:00
const sourceUuid = UUID . generate ( ) . toString ( ) ;
2020-12-04 20:41:40 +00:00
function getDefaultMessage ( id : string ) : MessageType {
return {
2021-06-17 17:15:10 +00:00
attachments : [ ] ,
2020-12-04 20:41:40 +00:00
conversationId : 'conversationId' ,
2021-06-17 17:15:10 +00:00
id ,
received_at : previousTime ,
sent_at : previousTime ,
2020-12-04 20:41:40 +00:00
source : 'source' ,
2021-10-26 22:59:08 +00:00
sourceUuid ,
2021-06-17 17:15:10 +00:00
timestamp : previousTime ,
2020-12-04 20:41:40 +00:00
type : 'incoming' as const ,
2021-08-12 18:15:55 +00:00
readStatus : ReadStatus.Read ,
2020-12-04 20:41:40 +00:00
} ;
}
function getDefaultConversationMessage ( ) : ConversationMessageType {
return {
heightChangeMessageIds : [ ] ,
isLoadingMessages : false ,
messageIds : [ ] ,
metrics : {
totalUnread : 0 ,
} ,
resetCounter : 0 ,
2021-11-23 13:09:07 +00:00
scrollToBottomCounter : 0 ,
2020-12-04 20:41:40 +00:00
scrollToMessageCounter : 0 ,
} ;
}
2021-02-23 20:34:28 +00:00
describe ( 'openConversationInternal' , ( ) = > {
it ( "returns a thunk that triggers a 'showConversation' event when passed a conversation ID" , ( ) = > {
const dispatch = sinon . spy ( ) ;
openConversationInternal ( { conversationId : 'abc123' } ) (
dispatch ,
getEmptyRootState ,
null
) ;
sinon . assert . calledOnce (
window . Whisper . events . trigger as sinon . SinonSpy
) ;
sinon . assert . calledWith (
window . Whisper . events . trigger as sinon . SinonSpy ,
'showConversation' ,
'abc123' ,
undefined
) ;
} ) ;
it ( "returns a thunk that triggers a 'showConversation' event when passed a conversation ID and message ID" , ( ) = > {
const dispatch = sinon . spy ( ) ;
openConversationInternal ( {
conversationId : 'abc123' ,
messageId : 'xyz987' ,
} ) ( dispatch , getEmptyRootState , null ) ;
sinon . assert . calledOnce (
window . Whisper . events . trigger as sinon . SinonSpy
) ;
sinon . assert . calledWith (
window . Whisper . events . trigger as sinon . SinonSpy ,
'showConversation' ,
'abc123' ,
'xyz987'
) ;
} ) ;
it ( "returns a thunk that doesn't dispatch any actions by default" , ( ) = > {
const dispatch = sinon . spy ( ) ;
openConversationInternal ( { conversationId : 'abc123' } ) (
dispatch ,
getEmptyRootState ,
null
) ;
sinon . assert . notCalled ( dispatch ) ;
} ) ;
it ( 'dispatches a SWITCH_TO_ASSOCIATED_VIEW action if called with a flag' , ( ) = > {
const dispatch = sinon . spy ( ) ;
openConversationInternal ( {
conversationId : 'abc123' ,
switchToAssociatedView : true ,
} ) ( dispatch , getEmptyRootState , null ) ;
sinon . assert . calledWith ( dispatch , {
type : 'SWITCH_TO_ASSOCIATED_VIEW' ,
payload : { conversationId : 'abc123' } ,
} ) ;
} ) ;
describe ( 'SWITCH_TO_ASSOCIATED_VIEW' , ( ) = > {
let action : SwitchToAssociatedViewActionType ;
beforeEach ( ( ) = > {
const dispatch = sinon . spy ( ) ;
openConversationInternal ( {
conversationId : 'fake-conversation-id' ,
switchToAssociatedView : true ,
} ) ( dispatch , getEmptyRootState , null ) ;
[ action ] = dispatch . getCall ( 0 ) . args ;
} ) ;
it ( 'shows the inbox if the conversation is not archived' , ( ) = > {
2021-05-07 22:21:10 +00:00
const conversation = getDefaultConversation ( {
id : 'fake-conversation-id' ,
} ) ;
2021-02-23 20:34:28 +00:00
const state = {
. . . getEmptyState ( ) ,
conversationLookup : {
2021-05-07 22:21:10 +00:00
[ conversation . id ] : conversation ,
2021-02-23 20:34:28 +00:00
} ,
} ;
const result = reducer ( state , action ) ;
assert . isUndefined ( result . composer ) ;
assert . isFalse ( result . showArchived ) ;
} ) ;
it ( 'shows the archive if the conversation is archived' , ( ) = > {
2021-05-07 22:21:10 +00:00
const conversation = getDefaultConversation ( {
id : 'fake-conversation-id' ,
isArchived : true ,
} ) ;
2021-02-23 20:34:28 +00:00
const state = {
. . . getEmptyState ( ) ,
conversationLookup : {
2021-05-07 22:21:10 +00:00
[ conversation . id ] : conversation ,
2021-02-23 20:34:28 +00:00
} ,
} ;
const result = reducer ( state , action ) ;
assert . isUndefined ( result . composer ) ;
assert . isTrue ( result . showArchived ) ;
} ) ;
it ( 'does nothing if the conversation is not found' , ( ) = > {
const state = getEmptyState ( ) ;
const result = reducer ( state , action ) ;
assert . strictEqual ( result , state ) ;
} ) ;
} ) ;
} ) ;
2021-03-03 20:09:58 +00:00
describe ( 'CANT_ADD_CONTACT_TO_GROUP' , ( ) = > {
it ( 'marks the conversation ID as "cannot add"' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = cantAddContactToGroup ( 'abc123' ) ;
const result = reducer ( state , action ) ;
assert (
result . composer ? . step === ComposerStep . ChooseGroupMembers &&
result . composer . cantAddContactIdForModal === 'abc123'
) ;
} ) ;
} ) ;
describe ( 'CLEAR_GROUP_CREATION_ERROR' , ( ) = > {
it ( 'clears the group creation error' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultSetGroupMetadataComposerState ,
2021-03-03 20:09:58 +00:00
hasError : true as const ,
} ,
} ;
const action = clearGroupCreationError ( ) ;
const result = reducer ( state , action ) ;
assert (
result . composer ? . step === ComposerStep . SetGroupMetadata &&
result . composer . hasError === false
) ;
} ) ;
} ) ;
2021-10-26 22:59:08 +00:00
describe ( 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' , ( ) = > {
it ( 'clears the list of invited conversation UUIDs' , ( ) = > {
2021-03-03 20:09:58 +00:00
const state = {
. . . getEmptyState ( ) ,
2021-10-26 22:59:08 +00:00
invitedUuidsForNewlyCreatedGroup : [
UUID . generate ( ) . toString ( ) ,
UUID . generate ( ) . toString ( ) ,
] ,
2021-03-03 20:09:58 +00:00
} ;
2021-10-26 22:59:08 +00:00
const action = clearInvitedUuidsForNewlyCreatedGroup ( ) ;
2021-03-03 20:09:58 +00:00
const result = reducer ( state , action ) ;
2021-10-26 22:59:08 +00:00
assert . isUndefined ( result . invitedUuidsForNewlyCreatedGroup ) ;
2021-03-03 20:09:58 +00:00
} ) ;
} ) ;
describe ( 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL' , ( ) = > {
it ( 'closes the "cannot add contact" modal"' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
cantAddContactIdForModal : 'abc123' ,
} ,
} ;
const action = closeCantAddContactToGroupModal ( ) ;
const result = reducer ( state , action ) ;
assert (
result . composer ? . step === ComposerStep . ChooseGroupMembers &&
result . composer . cantAddContactIdForModal === undefined ,
'Expected the contact ID to be cleared'
) ;
} ) ;
} ) ;
2021-04-21 16:31:12 +00:00
describe ( 'CLOSE_CONTACT_SPOOFING_REVIEW' , ( ) = > {
it ( 'closes the contact spoofing review modal if it was open' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
contactSpoofingReview : {
2021-06-01 23:30:25 +00:00
type : ContactSpoofingType . DirectConversationWithSameTitle as const ,
2021-04-21 16:31:12 +00:00
safeConversationId : 'abc123' ,
} ,
} ;
const action = closeContactSpoofingReview ( ) ;
const actual = reducer ( state , action ) ;
assert . isUndefined ( actual . contactSpoofingReview ) ;
} ) ;
it ( "does nothing if the modal wasn't already open" , ( ) = > {
const state = getEmptyState ( ) ;
const action = closeContactSpoofingReview ( ) ;
const actual = reducer ( state , action ) ;
assert . deepEqual ( actual , state ) ;
} ) ;
} ) ;
2021-03-03 20:09:58 +00:00
describe ( 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL' , ( ) = > {
it ( 'closes the maximum group size modal if it was open' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
maximumGroupSizeModalState : OneTimeModalState.Showing ,
} ,
} ;
const action = closeMaximumGroupSizeModal ( ) ;
const result = reducer ( state , action ) ;
assert (
result . composer ? . step === ComposerStep . ChooseGroupMembers &&
result . composer . maximumGroupSizeModalState ===
OneTimeModalState . Shown ,
'Expected the modal to be closed'
) ;
} ) ;
it ( 'does nothing if the maximum group size modal was never shown' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = closeMaximumGroupSizeModal ( ) ;
const result = reducer ( state , action ) ;
assert . strictEqual ( result , state ) ;
} ) ;
it ( 'does nothing if the maximum group size modal already closed' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
maximumGroupSizeModalState : OneTimeModalState.Shown ,
} ,
} ;
const action = closeMaximumGroupSizeModal ( ) ;
const result = reducer ( state , action ) ;
assert . strictEqual ( result , state ) ;
} ) ;
} ) ;
describe ( 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' , ( ) = > {
it ( 'closes the recommended group size modal if it was open' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
recommendedGroupSizeModalState : OneTimeModalState.Showing ,
} ,
} ;
const action = closeRecommendedGroupSizeModal ( ) ;
const result = reducer ( state , action ) ;
assert (
result . composer ? . step === ComposerStep . ChooseGroupMembers &&
result . composer . recommendedGroupSizeModalState ===
OneTimeModalState . Shown ,
'Expected the modal to be closed'
) ;
} ) ;
it ( 'does nothing if the recommended group size modal was never shown' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = closeRecommendedGroupSizeModal ( ) ;
const result = reducer ( state , action ) ;
assert . strictEqual ( result , state ) ;
} ) ;
it ( 'does nothing if the recommended group size modal already closed' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
recommendedGroupSizeModalState : OneTimeModalState.Shown ,
} ,
} ;
const action = closeRecommendedGroupSizeModal ( ) ;
const result = reducer ( state , action ) ;
assert . strictEqual ( result , state ) ;
} ) ;
} ) ;
describe ( 'createGroup' , ( ) = > {
const conversationsState = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultSetGroupMetadataComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ 'abc123' ] ,
groupName : 'Foo Bar Group' ,
2021-09-24 00:49:05 +00:00
groupAvatar : new Uint8Array ( [ 1 , 2 , 3 ] ) ,
2021-03-03 20:09:58 +00:00
} ,
} ;
it ( 'immediately dispatches a CREATE_GROUP_PENDING action, which puts the composer in a loading state' , ( ) = > {
const dispatch = sinon . spy ( ) ;
createGroup ( ) (
dispatch ,
( ) = > ( {
. . . getEmptyRootState ( ) ,
conversations : conversationsState ,
} ) ,
null
) ;
sinon . assert . calledOnce ( dispatch ) ;
sinon . assert . calledWith ( dispatch , { type : 'CREATE_GROUP_PENDING' } ) ;
const action = dispatch . getCall ( 0 ) . args [ 0 ] ;
const result = reducer ( conversationsState , action ) ;
assert (
result . composer ? . step === ComposerStep . SetGroupMetadata &&
result . composer . isCreating &&
! result . composer . hasError
) ;
} ) ;
it ( 'calls groups.createGroupV2' , async ( ) = > {
await createGroup ( ) (
sinon . spy ( ) ,
( ) = > ( {
. . . getEmptyRootState ( ) ,
conversations : conversationsState ,
} ) ,
null
) ;
sinon . assert . calledOnce ( createGroupStub ) ;
sinon . assert . calledWith ( createGroupStub , {
name : 'Foo Bar Group' ,
2021-09-24 00:49:05 +00:00
avatar : new Uint8Array ( [ 1 , 2 , 3 ] ) ,
2021-08-06 00:17:05 +00:00
avatars : [ ] ,
2021-06-25 23:52:56 +00:00
expireTimer : 0 ,
2021-03-03 20:09:58 +00:00
conversationIds : [ 'abc123' ] ,
} ) ;
} ) ;
2021-03-11 01:54:13 +00:00
it ( "trims the group's title before calling groups.createGroupV2" , async ( ) = > {
await createGroup ( ) (
sinon . spy ( ) ,
( ) = > ( {
. . . getEmptyRootState ( ) ,
conversations : {
. . . conversationsState ,
composer : {
. . . conversationsState . composer ,
groupName : ' To Trim \t' ,
} ,
} ,
} ) ,
null
) ;
sinon . assert . calledWith (
createGroupStub ,
sinon . match ( { name : 'To Trim' } )
) ;
} ) ;
2021-03-03 20:09:58 +00:00
it ( 'dispatches a CREATE_GROUP_REJECTED action if group creation fails, which marks the state with an error' , async ( ) = > {
createGroupStub . rejects ( new Error ( 'uh oh' ) ) ;
const dispatch = sinon . spy ( ) ;
const createGroupPromise = createGroup ( ) (
dispatch ,
( ) = > ( {
. . . getEmptyRootState ( ) ,
conversations : conversationsState ,
} ) ,
null
) ;
const pendingAction = dispatch . getCall ( 0 ) . args [ 0 ] ;
const stateAfterPending = reducer ( conversationsState , pendingAction ) ;
await createGroupPromise ;
sinon . assert . calledTwice ( dispatch ) ;
sinon . assert . calledWith ( dispatch , { type : 'CREATE_GROUP_REJECTED' } ) ;
const rejectedAction = dispatch . getCall ( 1 ) . args [ 0 ] ;
const result = reducer ( stateAfterPending , rejectedAction ) ;
assert (
result . composer ? . step === ComposerStep . SetGroupMetadata &&
! result . composer . isCreating &&
result . composer . hasError
) ;
} ) ;
it ( "when rejecting, does nothing to the left pane if it's no longer in this composer state" , async ( ) = > {
createGroupStub . rejects ( new Error ( 'uh oh' ) ) ;
const dispatch = sinon . spy ( ) ;
const createGroupPromise = createGroup ( ) (
dispatch ,
( ) = > ( {
. . . getEmptyRootState ( ) ,
conversations : conversationsState ,
} ) ,
null
) ;
await createGroupPromise ;
const state = getEmptyState ( ) ;
const rejectedAction = dispatch . getCall ( 1 ) . args [ 0 ] ;
const result = reducer ( state , rejectedAction ) ;
assert . strictEqual ( result , state ) ;
} ) ;
it ( 'dispatches a CREATE_GROUP_FULFILLED event (which updates the newly-created conversation IDs), triggers a showConversation event and switches to the associated conversation on success' , async ( ) = > {
2021-10-26 22:59:08 +00:00
const abc = UUID . fromPrefix ( 'abc' ) . toString ( ) ;
2021-03-03 20:09:58 +00:00
createGroupStub . resolves ( {
id : '9876' ,
get : ( key : string ) = > {
if ( key !== 'pendingMembersV2' ) {
throw new Error ( 'This getter is not set up for this test' ) ;
}
2021-10-26 22:59:08 +00:00
return [ { uuid : abc } ] ;
2021-03-03 20:09:58 +00:00
} ,
} ) ;
const dispatch = sinon . spy ( ) ;
await createGroup ( ) (
dispatch ,
( ) = > ( {
. . . getEmptyRootState ( ) ,
conversations : conversationsState ,
} ) ,
null
) ;
sinon . assert . calledWith (
window . Whisper . events . trigger as sinon . SinonSpy ,
'showConversation' ,
'9876' ,
undefined
) ;
sinon . assert . calledWith ( dispatch , {
type : 'CREATE_GROUP_FULFILLED' ,
2021-10-26 22:59:08 +00:00
payload : { invitedUuids : [ abc ] } ,
2021-03-03 20:09:58 +00:00
} ) ;
const fulfilledAction = dispatch . getCall ( 1 ) . args [ 0 ] ;
const result = reducer ( conversationsState , fulfilledAction ) ;
2021-10-26 22:59:08 +00:00
assert . deepEqual ( result . invitedUuidsForNewlyCreatedGroup , [ abc ] ) ;
2021-03-03 20:09:58 +00:00
sinon . assert . calledWith ( dispatch , {
type : 'SWITCH_TO_ASSOCIATED_VIEW' ,
payload : { conversationId : '9876' } ,
} ) ;
} ) ;
} ) ;
2020-12-07 20:43:19 +00:00
describe ( 'MESSAGE_SIZE_CHANGED' , ( ) = > {
const stateWithActiveConversation = {
2021-01-06 15:41:43 +00:00
. . . getEmptyState ( ) ,
2020-12-07 20:43:19 +00:00
messagesByConversation : {
[ conversationId ] : {
heightChangeMessageIds : [ ] ,
isLoadingMessages : false ,
isNearBottom : true ,
messageIds : [ messageId ] ,
metrics : { totalUnread : 0 } ,
resetCounter : 0 ,
2021-11-23 13:09:07 +00:00
scrollToBottomCounter : 0 ,
2020-12-07 20:43:19 +00:00
scrollToMessageCounter : 0 ,
} ,
} ,
messagesLookup : {
[ messageId ] : getDefaultMessage ( messageId ) ,
} ,
} ;
it ( 'does nothing if no conversation is active' , ( ) = > {
2021-01-06 15:41:43 +00:00
const state = getEmptyState ( ) ;
2020-12-07 20:43:19 +00:00
assert . strictEqual (
reducer ( state , messageSizeChanged ( 'messageId' , 'convoId' ) ) ,
state
) ;
} ) ;
it ( 'does nothing if a different conversation is active' , ( ) = > {
assert . deepEqual (
reducer (
stateWithActiveConversation ,
messageSizeChanged ( messageId , 'another-conversation-guid' )
) ,
stateWithActiveConversation
) ;
} ) ;
it ( 'adds the message ID to the list of messages with changed heights' , ( ) = > {
const result = reducer (
stateWithActiveConversation ,
messageSizeChanged ( messageId , conversationId )
) ;
assert . sameMembers (
result . messagesByConversation [ conversationId ]
? . heightChangeMessageIds || [ ] ,
[ messageId ]
) ;
} ) ;
it ( "doesn't add duplicates to the list of changed-heights messages" , ( ) = > {
const state = set (
[ 'messagesByConversation' , conversationId , 'heightChangeMessageIds' ] ,
[ messageId ] ,
stateWithActiveConversation
) ;
const result = reducer (
state ,
messageSizeChanged ( messageId , conversationId )
) ;
assert . sameMembers (
result . messagesByConversation [ conversationId ]
? . heightChangeMessageIds || [ ] ,
[ messageId ]
) ;
} ) ;
} ) ;
2021-08-31 20:58:39 +00:00
describe ( 'MESSAGE_STOPPED_BY_MISSING_VERIFICATION' , ( ) = > {
it ( 'adds messages that need conversation verification, removing duplicates' , ( ) = > {
const first = reducer (
getEmptyState ( ) ,
messageStoppedByMissingVerification ( 'message 1' , [ 'convo 1' ] )
) ;
const second = reducer (
first ,
messageStoppedByMissingVerification ( 'message 1' , [ 'convo 2' ] )
) ;
const third = reducer (
second ,
messageStoppedByMissingVerification ( 'message 2' , [
'convo 1' ,
'convo 3' ,
] )
) ;
assert . deepStrictEqual (
third . outboundMessagesPendingConversationVerification ,
{
'convo 1' : [ 'message 1' , 'message 2' ] ,
'convo 2' : [ 'message 1' ] ,
'convo 3' : [ 'message 2' ] ,
}
) ;
} ) ;
} ) ;
2020-12-04 20:41:40 +00:00
describe ( 'REPAIR_NEWEST_MESSAGE' , ( ) = > {
it ( 'updates newest' , ( ) = > {
const action = repairNewestMessage ( conversationId ) ;
const state : ConversationsStateType = {
2021-01-06 15:41:43 +00:00
. . . getEmptyState ( ) ,
2020-12-04 20:41:40 +00:00
messagesLookup : {
[ messageId ] : {
. . . getDefaultMessage ( messageId ) ,
received_at : time ,
2021-06-17 17:15:10 +00:00
sent_at : time ,
2020-12-04 20:41:40 +00:00
} ,
} ,
messagesByConversation : {
[ conversationId ] : {
. . . getDefaultConversationMessage ( ) ,
messageIds : [ messageIdThree , messageIdTwo , messageId ] ,
metrics : {
totalUnread : 0 ,
} ,
} ,
} ,
} ;
const expected : ConversationsStateType = {
2021-01-06 15:41:43 +00:00
. . . getEmptyState ( ) ,
2020-12-04 20:41:40 +00:00
messagesLookup : {
[ messageId ] : {
. . . getDefaultMessage ( messageId ) ,
received_at : time ,
2021-06-17 17:15:10 +00:00
sent_at : time ,
2020-12-04 20:41:40 +00:00
} ,
} ,
messagesByConversation : {
[ conversationId ] : {
. . . getDefaultConversationMessage ( ) ,
messageIds : [ messageIdThree , messageIdTwo , messageId ] ,
metrics : {
totalUnread : 0 ,
newest : {
id : messageId ,
received_at : time ,
2021-06-17 17:15:10 +00:00
sent_at : time ,
2020-12-04 20:41:40 +00:00
} ,
} ,
} ,
} ,
} ;
const actual = reducer ( state , action ) ;
assert . deepEqual ( actual , expected ) ;
} ) ;
it ( 'clears newest' , ( ) = > {
const action = repairNewestMessage ( conversationId ) ;
const state : ConversationsStateType = {
2021-01-06 15:41:43 +00:00
. . . getEmptyState ( ) ,
2020-12-04 20:41:40 +00:00
messagesLookup : {
[ messageId ] : {
. . . getDefaultMessage ( messageId ) ,
received_at : time ,
} ,
} ,
messagesByConversation : {
[ conversationId ] : {
. . . getDefaultConversationMessage ( ) ,
messageIds : [ ] ,
metrics : {
totalUnread : 0 ,
newest : {
id : messageId ,
received_at : time ,
} ,
} ,
} ,
} ,
} ;
const expected : ConversationsStateType = {
2021-01-06 15:41:43 +00:00
. . . getEmptyState ( ) ,
2020-12-04 20:41:40 +00:00
messagesLookup : {
[ messageId ] : {
. . . getDefaultMessage ( messageId ) ,
received_at : time ,
} ,
} ,
messagesByConversation : {
[ conversationId ] : {
. . . getDefaultConversationMessage ( ) ,
messageIds : [ ] ,
metrics : {
newest : undefined ,
totalUnread : 0 ,
} ,
} ,
} ,
} ;
const actual = reducer ( state , action ) ;
assert . deepEqual ( actual , expected ) ;
} ) ;
it ( 'returns state if conversation not present' , ( ) = > {
const action = repairNewestMessage ( conversationId ) ;
2021-01-06 15:41:43 +00:00
const state : ConversationsStateType = getEmptyState ( ) ;
2020-12-04 20:41:40 +00:00
const actual = reducer ( state , action ) ;
assert . equal ( actual , state ) ;
} ) ;
} ) ;
describe ( 'REPAIR_OLDEST_MESSAGE' , ( ) = > {
it ( 'updates oldest' , ( ) = > {
const action = repairOldestMessage ( conversationId ) ;
const state : ConversationsStateType = {
2021-01-06 15:41:43 +00:00
. . . getEmptyState ( ) ,
2020-12-04 20:41:40 +00:00
messagesLookup : {
[ messageId ] : {
. . . getDefaultMessage ( messageId ) ,
received_at : time ,
2021-06-17 17:15:10 +00:00
sent_at : time ,
2020-12-04 20:41:40 +00:00
} ,
} ,
messagesByConversation : {
[ conversationId ] : {
. . . getDefaultConversationMessage ( ) ,
messageIds : [ messageId , messageIdTwo , messageIdThree ] ,
metrics : {
totalUnread : 0 ,
} ,
} ,
} ,
} ;
const expected : ConversationsStateType = {
2021-01-06 15:41:43 +00:00
. . . getEmptyState ( ) ,
2020-12-04 20:41:40 +00:00
messagesLookup : {
[ messageId ] : {
. . . getDefaultMessage ( messageId ) ,
received_at : time ,
2021-06-17 17:15:10 +00:00
sent_at : time ,
2020-12-04 20:41:40 +00:00
} ,
} ,
messagesByConversation : {
[ conversationId ] : {
. . . getDefaultConversationMessage ( ) ,
messageIds : [ messageId , messageIdTwo , messageIdThree ] ,
metrics : {
totalUnread : 0 ,
oldest : {
id : messageId ,
received_at : time ,
2021-06-17 17:15:10 +00:00
sent_at : time ,
2020-12-04 20:41:40 +00:00
} ,
} ,
} ,
} ,
} ;
const actual = reducer ( state , action ) ;
assert . deepEqual ( actual , expected ) ;
} ) ;
it ( 'clears oldest' , ( ) = > {
const action = repairOldestMessage ( conversationId ) ;
const state : ConversationsStateType = {
2021-01-06 15:41:43 +00:00
. . . getEmptyState ( ) ,
2020-12-04 20:41:40 +00:00
messagesLookup : {
[ messageId ] : {
. . . getDefaultMessage ( messageId ) ,
received_at : time ,
} ,
} ,
messagesByConversation : {
[ conversationId ] : {
. . . getDefaultConversationMessage ( ) ,
messageIds : [ ] ,
metrics : {
totalUnread : 0 ,
oldest : {
id : messageId ,
received_at : time ,
} ,
} ,
} ,
} ,
} ;
const expected : ConversationsStateType = {
2021-01-06 15:41:43 +00:00
. . . getEmptyState ( ) ,
2020-12-04 20:41:40 +00:00
messagesLookup : {
[ messageId ] : {
. . . getDefaultMessage ( messageId ) ,
received_at : time ,
} ,
} ,
messagesByConversation : {
[ conversationId ] : {
. . . getDefaultConversationMessage ( ) ,
messageIds : [ ] ,
metrics : {
oldest : undefined ,
totalUnread : 0 ,
} ,
} ,
} ,
} ;
const actual = reducer ( state , action ) ;
assert . deepEqual ( actual , expected ) ;
} ) ;
it ( 'returns state if conversation not present' , ( ) = > {
const action = repairOldestMessage ( conversationId ) ;
2021-01-06 15:41:43 +00:00
const state : ConversationsStateType = getEmptyState ( ) ;
2020-12-04 20:41:40 +00:00
const actual = reducer ( state , action ) ;
assert . equal ( actual , state ) ;
} ) ;
} ) ;
2021-01-29 22:16:48 +00:00
2021-06-01 23:30:25 +00:00
describe ( 'REVIEW_GROUP_MEMBER_NAME_COLLISION' , ( ) = > {
it ( 'starts reviewing a group member name collision' , ( ) = > {
const state = getEmptyState ( ) ;
const action = reviewGroupMemberNameCollision ( 'abc123' ) ;
const actual = reducer ( state , action ) ;
assert . deepEqual ( actual . contactSpoofingReview , {
type : ContactSpoofingType . MultipleGroupMembersWithSameTitle as const ,
groupConversationId : 'abc123' ,
} ) ;
} ) ;
} ) ;
2021-04-21 16:31:12 +00:00
describe ( 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION' , ( ) = > {
it ( 'starts reviewing a message request name collision' , ( ) = > {
const state = getEmptyState ( ) ;
const action = reviewMessageRequestNameCollision ( {
safeConversationId : 'def' ,
} ) ;
const actual = reducer ( state , action ) ;
assert . deepEqual ( actual . contactSpoofingReview , {
2021-06-01 23:30:25 +00:00
type : ContactSpoofingType . DirectConversationWithSameTitle as const ,
2021-04-21 16:31:12 +00:00
safeConversationId : 'def' ,
} ) ;
} ) ;
} ) ;
2021-03-03 20:09:58 +00:00
describe ( 'SET_COMPOSE_GROUP_AVATAR' , ( ) = > {
it ( "can clear the composer's group avatar" , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultSetGroupMetadataComposerState ,
2021-09-24 00:49:05 +00:00
groupAvatar : new Uint8Array ( 2 ) ,
2021-03-03 20:09:58 +00:00
} ,
} ;
const action = setComposeGroupAvatar ( undefined ) ;
const result = reducer ( state , action ) ;
assert (
result . composer ? . step === ComposerStep . SetGroupMetadata &&
result . composer . groupAvatar === undefined
) ;
} ) ;
it ( "can set the composer's group avatar" , ( ) = > {
2021-09-24 00:49:05 +00:00
const avatar = new Uint8Array ( [ 1 , 2 , 3 ] ) ;
2021-03-03 20:09:58 +00:00
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultSetGroupMetadataComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = setComposeGroupAvatar ( avatar ) ;
const result = reducer ( state , action ) ;
assert (
result . composer ? . step === ComposerStep . SetGroupMetadata &&
result . composer . groupAvatar === avatar
) ;
} ) ;
} ) ;
describe ( 'SET_COMPOSE_GROUP_NAME' , ( ) = > {
it ( "can set the composer's group name" , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultSetGroupMetadataComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = setComposeGroupName ( 'bing bong' ) ;
const result = reducer ( state , action ) ;
assert (
result . composer ? . step === ComposerStep . SetGroupMetadata &&
result . composer . groupName === 'bing bong'
) ;
} ) ;
} ) ;
2021-02-23 20:34:28 +00:00
describe ( 'SET_COMPOSE_SEARCH_TERM' , ( ) = > {
it ( 'updates the contact search term' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultStartDirectConversationComposerState ,
2021-02-23 20:34:28 +00:00
} ;
2021-11-12 01:17:29 +00:00
const result = reducer ( state , setComposeSearchTerm ( 'foo bar' ) ) ;
2021-02-23 20:34:28 +00:00
2021-03-03 20:09:58 +00:00
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultStartDirectConversationComposerState ,
2021-04-20 23:16:49 +00:00
searchTerm : 'foo bar' ,
2021-03-03 20:09:58 +00:00
} ) ;
2021-02-23 20:34:28 +00:00
} ) ;
} ) ;
2021-01-29 22:16:48 +00:00
describe ( 'SET_PRE_JOIN_CONVERSATION' , ( ) = > {
const startState = {
. . . getEmptyState ( ) ,
} ;
it ( 'starts with empty value' , ( ) = > {
assert . isUndefined ( startState . preJoinConversation ) ;
} ) ;
it ( 'sets value as provided' , ( ) = > {
const preJoinConversation = {
title : 'Pre-join group!' ,
memberCount : 4 ,
approvalRequired : false ,
} ;
const stateWithData = reducer (
startState ,
setPreJoinConversation ( preJoinConversation )
) ;
assert . deepEqual (
stateWithData . preJoinConversation ,
preJoinConversation
) ;
const resetState = reducer (
stateWithData ,
setPreJoinConversation ( undefined )
) ;
assert . isUndefined ( resetState . preJoinConversation ) ;
} ) ;
} ) ;
2021-02-23 20:34:28 +00:00
describe ( 'SHOW_ARCHIVED_CONVERSATIONS' , ( ) = > {
it ( 'is a no-op when already at the archive' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
showArchived : true ,
} ;
const action = showArchivedConversations ( ) ;
const result = reducer ( state , action ) ;
assert . isTrue ( result . showArchived ) ;
assert . isUndefined ( result . composer ) ;
} ) ;
it ( 'switches from the inbox to the archive' , ( ) = > {
const state = getEmptyState ( ) ;
const action = showArchivedConversations ( ) ;
const result = reducer ( state , action ) ;
assert . isTrue ( result . showArchived ) ;
assert . isUndefined ( result . composer ) ;
} ) ;
it ( 'switches from the composer to the archive' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultStartDirectConversationComposerState ,
2021-02-23 20:34:28 +00:00
} ;
const action = showArchivedConversations ( ) ;
const result = reducer ( state , action ) ;
assert . isTrue ( result . showArchived ) ;
assert . isUndefined ( result . composer ) ;
} ) ;
} ) ;
describe ( 'SHOW_INBOX' , ( ) = > {
it ( 'is a no-op when already at the inbox' , ( ) = > {
const state = getEmptyState ( ) ;
const action = showInbox ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
assert . isUndefined ( result . composer ) ;
} ) ;
it ( 'switches from the archive to the inbox' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
showArchived : true ,
} ;
const action = showInbox ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
assert . isUndefined ( result . composer ) ;
} ) ;
it ( 'switches from the composer to the inbox' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultStartDirectConversationComposerState ,
2021-02-23 20:34:28 +00:00
} ;
const action = showInbox ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
assert . isUndefined ( result . composer ) ;
} ) ;
} ) ;
describe ( 'START_COMPOSING' , ( ) = > {
2021-03-03 20:09:58 +00:00
it ( 'does nothing if on the first step of the composer' , ( ) = > {
2021-02-23 20:34:28 +00:00
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultStartDirectConversationComposerState ,
2021-02-23 20:34:28 +00:00
} ;
const action = startComposing ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
2021-08-16 14:33:27 +00:00
assert . deepEqual (
result . composer ,
defaultStartDirectConversationComposerState
) ;
2021-03-03 20:09:58 +00:00
} ) ;
it ( 'if on the second step of the composer, goes back to the first step, clearing the search term' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-04-20 23:16:49 +00:00
searchTerm : 'to be cleared' ,
2021-03-03 20:09:58 +00:00
} ,
} ;
const action = startComposing ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
2021-08-16 14:33:27 +00:00
assert . deepEqual (
result . composer ,
defaultStartDirectConversationComposerState
) ;
2021-03-03 20:09:58 +00:00
} ) ;
it ( 'if on the third step of the composer, goes back to the first step, clearing everything' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultSetGroupMetadataComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = startComposing ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
2021-08-16 14:33:27 +00:00
assert . deepEqual (
result . composer ,
defaultStartDirectConversationComposerState
) ;
2021-02-23 20:34:28 +00:00
} ) ;
it ( 'switches from the inbox to the composer' , ( ) = > {
const state = getEmptyState ( ) ;
const action = startComposing ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
2021-08-16 14:33:27 +00:00
assert . deepEqual (
result . composer ,
defaultStartDirectConversationComposerState
) ;
2021-02-23 20:34:28 +00:00
} ) ;
it ( 'switches from the archive to the inbox' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
showArchived : true ,
} ;
const action = startComposing ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
2021-08-16 14:33:27 +00:00
assert . deepEqual (
result . composer ,
defaultStartDirectConversationComposerState
) ;
2021-03-03 20:09:58 +00:00
} ) ;
} ) ;
describe ( 'SHOW_CHOOSE_GROUP_MEMBERS' , ( ) = > {
it ( 'switches to the second step of the composer if on the first step' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultStartDirectConversationComposerState ,
2021-04-20 23:16:49 +00:00
searchTerm : 'to be cleared' ,
2021-03-03 20:09:58 +00:00
} ,
} ;
const action = showChooseGroupMembers ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-08-06 00:17:05 +00:00
userAvatarData : getDefaultAvatars ( true ) ,
2021-03-03 20:09:58 +00:00
} ) ;
} ) ;
it ( 'does nothing if already on the second step of the composer' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = showChooseGroupMembers ( ) ;
const result = reducer ( state , action ) ;
assert . strictEqual ( result , state ) ;
} ) ;
it ( 'returns to the second step if on the third step of the composer' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultSetGroupMetadataComposerState ,
2021-03-03 20:09:58 +00:00
groupName : 'Foo Bar Group' ,
2021-09-24 00:49:05 +00:00
groupAvatar : new Uint8Array ( [ 4 , 2 ] ) ,
2021-03-03 20:09:58 +00:00
} ,
} ;
const action = showChooseGroupMembers ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
groupName : 'Foo Bar Group' ,
2021-09-24 00:49:05 +00:00
groupAvatar : new Uint8Array ( [ 4 , 2 ] ) ,
2021-03-03 20:09:58 +00:00
} ) ;
} ) ;
it ( 'switches from the inbox to the second step of the composer' , ( ) = > {
const state = getEmptyState ( ) ;
const action = showChooseGroupMembers ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-08-06 00:17:05 +00:00
userAvatarData : getDefaultAvatars ( true ) ,
2021-03-03 20:09:58 +00:00
} ) ;
} ) ;
it ( 'switches from the archive to the second step of the composer' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
showArchived : true ,
} ;
const action = showChooseGroupMembers ( ) ;
const result = reducer ( state , action ) ;
assert . isFalse ( result . showArchived ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-08-06 00:17:05 +00:00
userAvatarData : getDefaultAvatars ( true ) ,
2021-03-03 20:09:58 +00:00
} ) ;
} ) ;
} ) ;
describe ( 'START_SETTING_GROUP_METADATA' , ( ) = > {
it ( 'moves from the second to the third step of the composer' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ 'abc' , 'def' ] ,
} ,
} ;
const action = startSettingGroupMetadata ( ) ;
const result = reducer ( state , action ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultSetGroupMetadataComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ 'abc' , 'def' ] ,
} ) ;
} ) ;
it ( 'maintains state when going from the second to third steps of the composer, if the second step already had some data (likely from a previous visit)' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-04-20 23:16:49 +00:00
searchTerm : 'foo bar' ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ 'abc' , 'def' ] ,
groupName : 'Foo Bar Group' ,
2021-09-24 00:49:05 +00:00
groupAvatar : new Uint8Array ( [ 6 , 9 ] ) ,
2021-03-03 20:09:58 +00:00
} ,
} ;
const action = startSettingGroupMetadata ( ) ;
const result = reducer ( state , action ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultSetGroupMetadataComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ 'abc' , 'def' ] ,
groupName : 'Foo Bar Group' ,
2021-09-24 00:49:05 +00:00
groupAvatar : new Uint8Array ( [ 6 , 9 ] ) ,
2021-03-03 20:09:58 +00:00
} ) ;
} ) ;
it ( 'does nothing if already on the third step of the composer' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultSetGroupMetadataComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = startSettingGroupMetadata ( ) ;
const result = reducer ( state , action ) ;
assert . strictEqual ( result , state ) ;
} ) ;
} ) ;
describe ( 'TOGGLE_CONVERSATION_IN_CHOOSE_MEMBERS' , ( ) = > {
function getAction (
id : string ,
conversationsState : ConversationsStateType
) : ToggleConversationInChooseMembersActionType {
const dispatch = sinon . spy ( ) ;
toggleConversationInChooseMembers ( id ) (
dispatch ,
( ) = > ( {
. . . getEmptyRootState ( ) ,
conversations : conversationsState ,
} ) ,
null
) ;
return dispatch . getCall ( 0 ) . args [ 0 ] ;
}
let remoteConfigGetValueStub : sinon.SinonStub ;
beforeEach ( ( ) = > {
remoteConfigGetValueStub = sinonSandbox
. stub ( window . Signal . RemoteConfig , 'getValue' )
. withArgs ( 'global.groupsv2.maxGroupSize' )
. returns ( '22' )
. withArgs ( 'global.groupsv2.groupSizeHardLimit' )
. returns ( '33' ) ;
} ) ;
it ( 'adds conversation IDs to the list' , ( ) = > {
const zero = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const one = reducer ( zero , getAction ( 'abc' , zero ) ) ;
const two = reducer ( one , getAction ( 'def' , one ) ) ;
assert . deepEqual ( two . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ 'abc' , 'def' ] ,
} ) ;
} ) ;
it ( 'removes conversation IDs from the list' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ 'abc' , 'def' ] ,
} ,
} ;
const action = getAction ( 'abc' , state ) ;
const result = reducer ( state , action ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ 'def' ] ,
} ) ;
} ) ;
it ( 'shows the recommended group size modal when first crossing the maximum recommended group size' , ( ) = > {
const oldSelectedConversationIds = times ( 21 , ( ) = > uuid ( ) ) ;
const newUuid = uuid ( ) ;
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : oldSelectedConversationIds ,
} ,
} ;
const action = getAction ( newUuid , state ) ;
const result = reducer ( state , action ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ . . . oldSelectedConversationIds , newUuid ] ,
recommendedGroupSizeModalState : OneTimeModalState.Showing ,
} ) ;
} ) ;
it ( "doesn't show the recommended group size modal twice" , ( ) = > {
const oldSelectedConversationIds = times ( 21 , ( ) = > uuid ( ) ) ;
const newUuid = uuid ( ) ;
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : oldSelectedConversationIds ,
recommendedGroupSizeModalState : OneTimeModalState.Shown ,
} ,
} ;
const action = getAction ( newUuid , state ) ;
const result = reducer ( state , action ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ . . . oldSelectedConversationIds , newUuid ] ,
recommendedGroupSizeModalState : OneTimeModalState.Shown ,
} ) ;
} ) ;
it ( 'defaults the maximum recommended size to 151' , ( ) = > {
[ undefined , 'xyz' ] . forEach ( value = > {
remoteConfigGetValueStub
. withArgs ( 'global.groupsv2.maxGroupSize' )
. returns ( value ) ;
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = getAction ( uuid ( ) , state ) ;
assert . strictEqual ( action . payload . maxRecommendedGroupSize , 151 ) ;
} ) ;
} ) ;
it ( 'shows the maximum group size modal when first reaching the maximum group size' , ( ) = > {
const oldSelectedConversationIds = times ( 31 , ( ) = > uuid ( ) ) ;
const newUuid = uuid ( ) ;
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : oldSelectedConversationIds ,
recommendedGroupSizeModalState : OneTimeModalState.Shown ,
maximumGroupSizeModalState : OneTimeModalState.NeverShown ,
} ,
} ;
const action = getAction ( newUuid , state ) ;
const result = reducer ( state , action ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ . . . oldSelectedConversationIds , newUuid ] ,
recommendedGroupSizeModalState : OneTimeModalState.Shown ,
maximumGroupSizeModalState : OneTimeModalState.Showing ,
} ) ;
} ) ;
it ( "doesn't show the maximum group size modal twice" , ( ) = > {
const oldSelectedConversationIds = times ( 31 , ( ) = > uuid ( ) ) ;
const newUuid = uuid ( ) ;
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : oldSelectedConversationIds ,
recommendedGroupSizeModalState : OneTimeModalState.Shown ,
maximumGroupSizeModalState : OneTimeModalState.Shown ,
} ,
} ;
const action = getAction ( newUuid , state ) ;
const result = reducer ( state , action ) ;
assert . deepEqual ( result . composer , {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : [ . . . oldSelectedConversationIds , newUuid ] ,
recommendedGroupSizeModalState : OneTimeModalState.Shown ,
maximumGroupSizeModalState : OneTimeModalState.Shown ,
} ) ;
} ) ;
it ( 'cannot select more than the maximum number of conversations' , ( ) = > {
const state = {
. . . getEmptyState ( ) ,
composer : {
2021-08-16 14:33:27 +00:00
. . . defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
selectedConversationIds : times ( 1000 , ( ) = > uuid ( ) ) ,
} ,
} ;
const action = getAction ( uuid ( ) , state ) ;
const result = reducer ( state , action ) ;
2021-03-11 21:29:31 +00:00
assert . deepEqual ( result , state ) ;
2021-03-03 20:09:58 +00:00
} ) ;
it ( 'defaults the maximum group size to 1001 if the recommended maximum is smaller' , ( ) = > {
[ undefined , 'xyz' ] . forEach ( value = > {
remoteConfigGetValueStub
. withArgs ( 'global.groupsv2.maxGroupSize' )
. returns ( '2' )
. withArgs ( 'global.groupsv2.groupSizeHardLimit' )
. returns ( value ) ;
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = getAction ( uuid ( ) , state ) ;
assert . strictEqual ( action . payload . maxGroupSize , 1001 ) ;
} ) ;
} ) ;
it ( 'defaults the maximum group size to (recommended maximum + 1) if the recommended maximum is more than 1001' , ( ) = > {
remoteConfigGetValueStub
. withArgs ( 'global.groupsv2.maxGroupSize' )
. returns ( '1234' )
. withArgs ( 'global.groupsv2.groupSizeHardLimit' )
. returns ( '2' ) ;
const state = {
. . . getEmptyState ( ) ,
2021-08-16 14:33:27 +00:00
composer : defaultChooseGroupMembersComposerState ,
2021-03-03 20:09:58 +00:00
} ;
const action = getAction ( uuid ( ) , state ) ;
assert . strictEqual ( action . payload . maxGroupSize , 1235 ) ;
2021-02-23 20:34:28 +00:00
} ) ;
} ) ;
2020-12-04 20:41:40 +00:00
} ) ;
2021-05-28 16:15:17 +00:00
describe ( 'COLORS_CHANGED' , ( ) = > {
2021-10-26 22:59:08 +00:00
const abc = getDefaultConversationWithUuid ( {
2021-05-28 16:15:17 +00:00
id : 'abc' ,
conversationColor : 'wintergreen' ,
} ) ;
2021-10-26 22:59:08 +00:00
const def = getDefaultConversationWithUuid ( {
2021-05-28 16:15:17 +00:00
id : 'def' ,
conversationColor : 'infrared' ,
} ) ;
const ghi = getDefaultConversation ( {
id : 'ghi' ,
e164 : 'ghi' ,
conversationColor : 'ember' ,
} ) ;
const jkl = getDefaultConversation ( {
id : 'jkl' ,
groupId : 'jkl' ,
conversationColor : 'plum' ,
} ) ;
const getState = ( ) = > ( {
. . . getEmptyRootState ( ) ,
conversations : {
. . . getEmptyState ( ) ,
conversationLookup : {
abc ,
def ,
ghi ,
jkl ,
} ,
conversationsByUuid : {
abc ,
def ,
} ,
conversationsByE164 : {
ghi ,
} ,
conversationsByGroupId : {
jkl ,
} ,
} ,
} ) ;
2021-06-02 21:05:09 +00:00
it ( 'resetAllChatColors' , async ( ) = > {
2021-05-28 16:15:17 +00:00
const dispatch = sinon . spy ( ) ;
2021-06-02 21:05:09 +00:00
await resetAllChatColors ( ) ( dispatch , getState , null ) ;
2021-05-28 16:15:17 +00:00
const [ action ] = dispatch . getCall ( 0 ) . args ;
const nextState = reducer ( getState ( ) . conversations , action ) ;
sinon . assert . calledOnce ( dispatch ) ;
2021-08-25 00:15:12 +00:00
assert . isUndefined ( nextState . conversationLookup . abc . conversationColor ) ;
assert . isUndefined ( nextState . conversationLookup . def . conversationColor ) ;
assert . isUndefined ( nextState . conversationLookup . ghi . conversationColor ) ;
assert . isUndefined ( nextState . conversationLookup . jkl . conversationColor ) ;
2021-10-26 22:59:08 +00:00
assert . isUndefined (
nextState . conversationsByUuid [ abc . uuid ] . conversationColor
) ;
assert . isUndefined (
nextState . conversationsByUuid [ def . uuid ] . conversationColor
) ;
2021-08-25 00:15:12 +00:00
assert . isUndefined ( nextState . conversationsByE164 . ghi . conversationColor ) ;
assert . isUndefined (
nextState . conversationsByGroupId . jkl . conversationColor
2021-05-28 16:15:17 +00:00
) ;
2021-06-02 21:05:09 +00:00
window . storage . remove ( 'defaultConversationColor' ) ;
2021-05-28 16:15:17 +00:00
} ) ;
} ) ;
2020-12-04 20:41:40 +00:00
} ) ;