diff --git a/ts/components/CallManager.stories.tsx b/ts/components/CallManager.stories.tsx index dbda846d6c39..49ece66acf26 100644 --- a/ts/components/CallManager.stories.tsx +++ b/ts/components/CallManager.stories.tsx @@ -24,6 +24,7 @@ import { setupI18n } from '../util/setupI18n'; import type { SafetyNumberProps } from './SafetyNumberChangeDialog'; import enMessages from '../../_locales/en/messages.json'; import { ThemeType } from '../types/Util'; +import { StorySendMode } from '../types/Stories'; const i18n = setupI18n('en', enMessages); @@ -194,6 +195,8 @@ export function RingingGroupCall(): JSX.Element { ...getConversation(), type: 'group', title: 'Tahoe Trip', + acknowledgedGroupNameCollisions: {}, + storySendMode: StorySendMode.IfActive, }, otherMembersRung: [ { firstName: 'Morty', title: 'Morty Smith' }, diff --git a/ts/components/conversation/conversation-details/PendingInvites.stories.tsx b/ts/components/conversation/conversation-details/PendingInvites.stories.tsx index d11daea26e23..6a75baad744f 100644 --- a/ts/components/conversation/conversation-details/PendingInvites.stories.tsx +++ b/ts/components/conversation/conversation-details/PendingInvites.stories.tsx @@ -7,6 +7,7 @@ import { times } from 'lodash'; import { action } from '@storybook/addon-actions'; import { UUID } from '../../../types/UUID'; +import { StorySendMode } from '../../../types/Stories'; import { setupI18n } from '../../../util/setupI18n'; import enMessages from '../../../../_locales/en/messages.json'; import type { PropsType } from './PendingInvites'; @@ -40,6 +41,8 @@ const conversation: ConversationType = { title: 'Some Conversation', type: 'group', sharedGroupNames: [], + acknowledgedGroupNameCollisions: {}, + storySendMode: StorySendMode.IfActive, }; const OUR_UUID = UUID.generate().toString(); diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index dd4e6566c98f..fd50bb7a3b65 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -1884,7 +1884,6 @@ export class ConversationModel extends window.Backbone groupVersion, groupId: this.get('groupId'), groupLink: this.getGroupLink(), - storySendMode: this.getStorySendMode(), hideStory: Boolean(this.get('hideStory')), inboxPosition, isArchived: this.get('isArchived'), @@ -1945,6 +1944,7 @@ export class ConversationModel extends window.Backbone acknowledgedGroupNameCollisions: this.get('acknowledgedGroupNameCollisions') || {}, sharedGroupNames: [], + storySendMode: this.getGroupStorySendMode(), }), voiceNotePlaybackRate: this.get('voiceNotePlaybackRate'), }; @@ -5548,10 +5548,21 @@ export class ConversationModel extends window.Backbone /** @return only undefined if not a group */ getStorySendMode(): StorySendMode | undefined { - if (!isGroup(this.attributes)) { + // isDirectConversation is used instead of isGroup because this is what + // used in `format()` when sending conversation "type" to redux. + if (isDirectConversation(this.attributes)) { return undefined; } + return this.getGroupStorySendMode(); + } + + private getGroupStorySendMode(): StorySendMode { + strictAssert( + !isDirectConversation(this.attributes), + 'Must be a group to have send story mode' + ); + return this.get('storySendMode') ?? StorySendMode.IfActive; } } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 0b48a35a9521..efec224467ea 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -192,7 +192,6 @@ export type ConversationType = { bannedMemberships?: Array; muteExpiresAt?: number; dontNotifyForMentionsIfMuted?: boolean; - type: ConversationTypeType; isMe: boolean; lastUpdated?: number; // This is used by the CompositionInput for @mentions @@ -217,12 +216,10 @@ export type ConversationType = { groupVersion?: 1 | 2; groupId?: string; groupLink?: string; - storySendMode?: StorySendMode; messageRequestsEnabled?: boolean; acceptedMessageRequest: boolean; secretParams?: string; publicParams?: string; - acknowledgedGroupNameCollisions?: GroupNameCollisionsWithIdsByTitle; profileKey?: string; voiceNotePlaybackRate?: number; @@ -236,7 +233,18 @@ export type ConversationType = { isVisible: boolean; } >; -}; +} & ( + | { + type: 'direct'; + storySendMode?: undefined; + acknowledgedGroupNameCollisions?: undefined; + } + | { + type: 'group'; + storySendMode: StorySendMode; + acknowledgedGroupNameCollisions: GroupNameCollisionsWithIdsByTitle; + } +); export type ProfileDataType = { firstName: string; } & Pick; diff --git a/ts/state/smart/Timeline.tsx b/ts/state/smart/Timeline.tsx index c14f917ab100..d4c360d96582 100644 --- a/ts/state/smart/Timeline.tsx +++ b/ts/state/smart/Timeline.tsx @@ -214,7 +214,7 @@ const getWarning = ( return { type: ContactSpoofingType.MultipleGroupMembersWithSameTitle, acknowledgedGroupNameCollisions: - conversation.acknowledgedGroupNameCollisions || {}, + conversation.acknowledgedGroupNameCollisions, groupNameCollisions: dehydrateCollisionsWithConversations(groupNameCollisions), }; @@ -223,7 +223,7 @@ const getWarning = ( return undefined; } default: - throw missingCaseError(conversation.type); + throw missingCaseError(conversation); } }; @@ -251,6 +251,10 @@ const getContactSpoofingReview = ( ), }; case ContactSpoofingType.MultipleGroupMembersWithSameTitle: { + assertDev( + currentConversation.type === 'group', + 'MultipleGroupMembersWithSameTitle: expects group conversation' + ); const { memberships } = getGroupMemberships( currentConversation, getConversationByUuid @@ -258,7 +262,7 @@ const getContactSpoofingReview = ( const groupNameCollisions = getCollisionsFromMemberships(memberships); const previouslyAcknowledgedTitlesById = invertIdsByTitle( - currentConversation.acknowledgedGroupNameCollisions || {} + currentConversation.acknowledgedGroupNameCollisions ); const collisionInfoByTitle = mapValues( diff --git a/ts/test-both/helpers/getDefaultConversation.ts b/ts/test-both/helpers/getDefaultConversation.ts index a0ebe192f6e1..7e86df542d78 100644 --- a/ts/test-both/helpers/getDefaultConversation.ts +++ b/ts/test-both/helpers/getDefaultConversation.ts @@ -8,6 +8,7 @@ import { UUID } from '../../types/UUID'; import type { UUIDStringType } from '../../types/UUID'; import { getRandomColor } from './getRandomColor'; import { ConversationColors } from '../../types/Colors'; +import { StorySendMode } from '../../types/Stories'; export const getAvatarPath = (): string => sample([ @@ -37,9 +38,11 @@ export function getDefaultConversation( sharedGroupNames: [], title: `${firstName} ${lastName}`, titleNoDefault: `${firstName} ${lastName}`, - type: 'direct' as const, uuid: UUID.generate().toString(), ...overrideProps, + type: 'direct' as const, + acknowledgedGroupNameCollisions: undefined, + storySendMode: undefined, }; } @@ -70,9 +73,11 @@ export function getDefaultGroup( memberships, sharedGroupNames: [], title: casual.title, - type: 'group' as const, uuid: UUID.generate().toString(), + acknowledgedGroupNameCollisions: {}, + storySendMode: StorySendMode.IfActive, ...overrideProps, + type: 'group' as const, }; } diff --git a/ts/test-both/state/selectors/conversations_test.ts b/ts/test-both/state/selectors/conversations_test.ts index 82d95df9e904..b27acef908e9 100644 --- a/ts/test-both/state/selectors/conversations_test.ts +++ b/ts/test-both/state/selectors/conversations_test.ts @@ -51,6 +51,7 @@ import type { UUIDStringType } from '../../../types/UUID'; import enMessages from '../../../../_locales/en/messages.json'; import { getDefaultConversation, + getDefaultGroup, getDefaultConversationWithUuid, } from '../../helpers/getDefaultConversation'; import { @@ -77,6 +78,16 @@ describe('both/state/selectors/conversations-extra', () => { }); } + function makeGroup(id: string): ConversationType { + const title = `${id} title`; + return getDefaultGroup({ + id, + searchableTitle: title, + title, + titleNoDefault: title, + }); + } + function makeConversationWithUuid( id: string ): ConversationType & { uuid: UUIDStringType } { @@ -535,15 +546,13 @@ describe('both/state/selectors/conversations-extra', () => { title: 'A', }, 'convo-2': { - ...makeConversation('convo-2'), - type: 'group', + ...makeGroup('convo-2'), isGroupV1AndDisabled: true, name: '2', title: 'Should Be Dropped (GV1)', }, 'convo-3': { - ...makeConversation('convo-3'), - type: 'group', + ...makeGroup('convo-3'), name: 'B', title: 'B', }, @@ -624,10 +633,8 @@ describe('both/state/selectors/conversations-extra', () => { profileSharing: false, }, 'convo-1': { - ...makeConversation('convo-1'), - type: 'group' as const, + ...makeGroup('convo-1'), name: 'Friends!', - sharedGroupNames: [], }, 'convo-2': { ...makeConversation('convo-2'), @@ -707,10 +714,8 @@ describe('both/state/selectors/conversations-extra', () => { name: 'Me!', }, 'convo-1': { - ...makeConversation('convo-1'), - type: 'group' as const, + ...makeGroup('convo-1'), name: 'Friends!', - sharedGroupNames: [], }, 'convo-2': { ...makeConversation('convo-2'), @@ -790,15 +795,11 @@ describe('both/state/selectors/conversations-extra', () => { name: 'Me!', }, 'convo-1': { - ...makeConversation('convo-1'), - type: 'group' as const, + ...makeGroup('convo-1'), name: 'Friends!', - sharedGroupNames: [], }, 'convo-2': { - ...makeConversation('convo-2'), - type: 'group' as const, - sharedGroupNames: [], + ...makeGroup('convo-2'), }, }, }, @@ -816,22 +817,16 @@ describe('both/state/selectors/conversations-extra', () => { ...getEmptyState(), conversationLookup: { 'convo-0': { - ...makeConversation('convo-0'), - type: 'group' as const, + ...makeGroup('convo-0'), name: 'Family!', isBlocked: true, - sharedGroupNames: [], }, 'convo-1': { - ...makeConversation('convo-1'), - type: 'group' as const, + ...makeGroup('convo-1'), name: 'Friends!', - sharedGroupNames: [], }, 'convo-2': { - ...makeConversation('convo-2'), - type: 'group' as const, - sharedGroupNames: [], + ...makeGroup('convo-2'), }, }, }, @@ -886,8 +881,7 @@ describe('both/state/selectors/conversations-extra', () => { title: 'Should Be Dropped (no name, no profile sharing)', }, 'convo-3': { - ...makeConversation('convo-3'), - type: 'group', + ...makeGroup('convo-3'), title: 'Should Be Dropped (group)', }, 'convo-4': { @@ -983,39 +977,29 @@ describe('both/state/selectors/conversations-extra', () => { title: 'Should be dropped (contact)', }, 'convo-3': { - ...makeConversation('convo-3'), - type: 'group', + ...makeGroup('convo-3'), name: 'Hello World', title: 'Hello World', - sharedGroupNames: [], }, 'convo-4': { - ...makeConversation('convo-4'), - type: 'group', + ...makeGroup('convo-4'), isBlocked: true, title: 'Should be dropped (blocked)', - sharedGroupNames: [], }, 'convo-5': { - ...makeConversation('convo-5'), - type: 'group', + ...makeGroup('convo-5'), title: 'Unknown Group', - sharedGroupNames: [], }, 'convo-6': { - ...makeConversation('convo-6'), - type: 'group', + ...makeGroup('convo-6'), name: 'Signal', title: 'Signal', - sharedGroupNames: [], }, 'convo-7': { - ...makeConversation('convo-7'), + ...makeGroup('convo-7'), profileSharing: false, - type: 'group', name: 'Signal Fake', title: 'Signal Fake', - sharedGroupNames: [], }, }, composer: { @@ -1070,10 +1054,8 @@ describe('both/state/selectors/conversations-extra', () => { title: 'Should be dropped (has no name)', }, 'convo-3': { - ...makeConversation('convo-3'), - type: 'group', + ...makeGroup('convo-3'), title: 'Should Be Dropped (group)', - sharedGroupNames: [], }, 'convo-4': { ...makeConversation('convo-4'), @@ -1654,17 +1636,18 @@ describe('both/state/selectors/conversations-extra', () => { describe('#getContactNameColorSelector', () => { it('returns the right color order sorted by UUID ASC', () => { - const group = makeConversation('group'); - group.type = 'group'; - group.sortedGroupMembers = [ - makeConversationWithUuid('fff'), - makeConversationWithUuid('f00'), - makeConversationWithUuid('e00'), - makeConversationWithUuid('d00'), - makeConversationWithUuid('c00'), - makeConversationWithUuid('b00'), - makeConversationWithUuid('a00'), - ]; + const group: ConversationType = { + ...makeGroup('group'), + sortedGroupMembers: [ + makeConversationWithUuid('fff'), + makeConversationWithUuid('f00'), + makeConversationWithUuid('e00'), + makeConversationWithUuid('d00'), + makeConversationWithUuid('c00'), + makeConversationWithUuid('b00'), + makeConversationWithUuid('a00'), + ], + }; const state = { ...getEmptyRootState(), conversations: { diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts index 5cd43bb39795..dde27ae5c07d 100644 --- a/ts/test-electron/state/ducks/conversations_test.ts +++ b/ts/test-electron/state/ducks/conversations_test.ts @@ -38,6 +38,7 @@ import { UUID } from '../../../types/UUID'; import { getDefaultConversation, getDefaultConversationWithUuid, + getDefaultGroup, } from '../../../test-both/helpers/getDefaultConversation'; import { getDefaultAvatars } from '../../../types/Avatar'; import { @@ -111,6 +112,7 @@ describe('both/state/ducks/conversations', () => { describe('helpers', () => { describe('getConversationCallMode', () => { const fakeConversation: ConversationType = getDefaultConversation(); + const fakeGroup: ConversationType = getDefaultGroup(); it("returns CallMode.None if you've left the conversation", () => { assert.strictEqual( @@ -155,19 +157,16 @@ describe('both/state/ducks/conversations', () => { it('returns CallMode.None for v1 groups', () => { assert.strictEqual( getConversationCallMode({ - ...fakeConversation, - type: 'group', + ...fakeGroup, groupVersion: 1, - sharedGroupNames: [], }), CallMode.None ); assert.strictEqual( getConversationCallMode({ - ...fakeConversation, - type: 'group', - sharedGroupNames: [], + ...fakeGroup, + groupVersion: undefined, }), CallMode.None ); @@ -183,10 +182,8 @@ describe('both/state/ducks/conversations', () => { it('returns CallMode.Group if the conversation is a v2 group', () => { assert.strictEqual( getConversationCallMode({ - ...fakeConversation, - type: 'group', + ...fakeGroup, groupVersion: 2, - sharedGroupNames: [], }), CallMode.Group ); diff --git a/ts/test-electron/state/selectors/messages_test.ts b/ts/test-electron/state/selectors/messages_test.ts index 4ac38be4a361..eef851843040 100644 --- a/ts/test-electron/state/selectors/messages_test.ts +++ b/ts/test-electron/state/selectors/messages_test.ts @@ -10,6 +10,10 @@ import type { ShallowChallengeError, } from '../../../model-types.d'; import type { ConversationType } from '../../../state/ducks/conversations'; +import { + getDefaultConversation, + getDefaultGroup, +} from '../../../test-both/helpers/getDefaultConversation'; import { canDeleteForEveryone, @@ -156,15 +160,7 @@ describe('state/selectors/messages', () => { }); describe('canReact', () => { - const defaultConversation: ConversationType = { - id: uuid(), - type: 'direct', - title: 'Test conversation', - isMe: false, - sharedGroupNames: [], - acceptedMessageRequest: true, - badges: [], - }; + const defaultConversation = getDefaultConversation(); it('returns false for disabled v1 groups', () => { const message = { @@ -172,8 +168,7 @@ describe('state/selectors/messages', () => { type: 'incoming' as const, }; const getConversationById = () => ({ - ...defaultConversation, - type: 'group' as const, + ...getDefaultGroup(), isGroupV1AndDisabled: true, }); @@ -249,8 +244,7 @@ describe('state/selectors/messages', () => { }, }; const getConversationById = () => ({ - ...defaultConversation, - type: 'group' as const, + ...getDefaultGroup(), }); assert.isTrue(canReact(message, ourConversationId, getConversationById)); @@ -284,8 +278,7 @@ describe('state/selectors/messages', () => { type: 'incoming' as const, }; const getConversationById = () => ({ - ...defaultConversation, - type: 'group' as const, + ...getDefaultGroup(), isGroupV1AndDisabled: true, }); @@ -360,10 +353,7 @@ describe('state/selectors/messages', () => { }, }, }; - const getConversationById = () => ({ - ...defaultConversation, - type: 'group' as const, - }); + const getConversationById = () => getDefaultGroup(); assert.isTrue(canReply(message, ourConversationId, getConversationById)); }); diff --git a/ts/util/findAndFormatContact.ts b/ts/util/findAndFormatContact.ts index 92f7800eb5da..8401c6ad96b5 100644 --- a/ts/util/findAndFormatContact.ts +++ b/ts/util/findAndFormatContact.ts @@ -4,20 +4,7 @@ import type { ConversationType } from '../state/ducks/conversations'; import { format, isValidNumber } from '../types/PhoneNumber'; -type FormattedContact = Partial & - Pick< - ConversationType, - | 'acceptedMessageRequest' - | 'badges' - | 'id' - | 'isMe' - | 'sharedGroupNames' - | 'title' - | 'type' - | 'unblurredAvatarPath' - >; - -const PLACEHOLDER_CONTACT: FormattedContact = { +const PLACEHOLDER_CONTACT: ConversationType = { acceptedMessageRequest: false, badges: [], id: 'placeholder-contact', @@ -27,7 +14,7 @@ const PLACEHOLDER_CONTACT: FormattedContact = { type: 'direct', }; -export function findAndFormatContact(identifier?: string): FormattedContact { +export function findAndFormatContact(identifier?: string): ConversationType { if (!identifier) { return PLACEHOLDER_CONTACT; }