@@ -81,21 +83,30 @@ const renderMembershipRow = ({
if (phoneNumber) {
return null;
}
- return
-
- {i18n('icu:no-groups-in-common-warning')}
+
+
+
+ {i18n('icu:no-groups-in-common-warning')}
+
+ {
+ ev.preventDefault();
+ onClickMessageRequestWarning();
+ }}
+ >
+ {i18n('icu:MessageRequestWarning__learn-more')}
+
-
- {i18n('icu:MessageRequestWarning__learn-more')}
-
);
};
@@ -115,7 +126,6 @@ export function ConversationHero({
isSignalConversation,
membersCount,
sharedGroupNames = [],
- name,
phoneNumber,
profileName,
theme,
@@ -124,6 +134,7 @@ export function ConversationHero({
unblurredAvatarPath,
updateSharedGroups,
viewUserStories,
+ toggleAboutContactModal,
}: Props): JSX.Element {
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
useState(false);
@@ -158,9 +169,29 @@ export function ConversationHero({
};
}
- const phoneNumberOnly = Boolean(
- !name && !profileName && conversationType === 'direct'
- );
+ let titleElem: JSX.Element | undefined;
+
+ if (isMe) {
+ titleElem = <>{i18n('icu:noteToSelf')}>;
+ } else if (isSignalConversation || conversationType !== 'direct') {
+ titleElem = (
+
+ );
+ } else if (title) {
+ titleElem = (
+
{
+ ev.preventDefault();
+ toggleAboutContactModal(id);
+ }}
+ >
+
+
+
+ );
+ }
/* eslint-disable no-nested-ternary */
return (
@@ -187,14 +218,7 @@ export function ConversationHero({
title={title}
/>
- {isMe ? (
- i18n('icu:noteToSelf')
- ) : (
-
- )}
+ {titleElem}
{isMe && }
{about && !isMe && (
@@ -212,9 +236,7 @@ export function ConversationHero({
/>
) : membersCount != null ? (
i18n('icu:ConversationHero--members', { count: membersCount })
- ) : phoneNumberOnly ? null : (
- phoneNumber
- )}
+ ) : null}
) : null}
{!isSignalConversation &&
diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx
index b8c1e2d8a9..0670893258 100644
--- a/ts/components/conversation/Timeline.stories.tsx
+++ b/ts/components/conversation/Timeline.stories.tsx
@@ -13,10 +13,8 @@ import type { PropsType } from './Timeline';
import { Timeline } from './Timeline';
import type { TimelineItemType } from './TimelineItem';
import { TimelineItem } from './TimelineItem';
-import { ContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
import { ConversationHero } from './ConversationHero';
-import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from '../../state/smart/ContactSpoofingReviewDialog';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { TypingBubble } from './TypingBubble';
import { ContactSpoofingType } from '../../util/contactSpoofing';
@@ -26,9 +24,13 @@ import { ThemeType } from '../../types/Util';
import { TextDirection } from './Message';
import { PaymentEventKind } from '../../types/Payment';
import type { PropsData as TimelineMessageProps } from './TimelineMessage';
+import { CollidingAvatars } from '../CollidingAvatars';
const i18n = setupI18n('en', enMessages);
+const alice = getDefaultConversation();
+const bob = getDefaultConversation();
+
export default {
title: 'Components/Conversation/Timeline',
argTypes: {},
@@ -323,10 +325,7 @@ const actions = () => ({
returnToActiveCall: action('returnToActiveCall'),
closeContactSpoofingReview: action('closeContactSpoofingReview'),
- reviewGroupMemberNameCollision: action('reviewGroupMemberNameCollision'),
- reviewMessageRequestNameCollision: action(
- 'reviewMessageRequestNameCollision'
- ),
+ reviewConversationNameCollision: action('reviewConversationNameCollision'),
unblurAvatar: action('unblurAvatar'),
@@ -375,35 +374,9 @@ const renderItem = ({
/>
);
-const renderContactSpoofingReviewDialog = (
- props: SmartContactSpoofingReviewDialogPropsType
-) => {
- const sharedProps = {
- acceptConversation: action('acceptConversation'),
- blockAndReportSpam: action('blockAndReportSpam'),
- blockConversation: action('blockConversation'),
- deleteConversation: action('deleteConversation'),
- getPreferredBadge: () => undefined,
- i18n,
- removeMember: action('removeMember'),
- showContactModal: action('showContactModal'),
- theme: ThemeType.dark,
- };
-
- if (props.type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) {
- return (
-
- );
- }
-
- return
;
+const renderContactSpoofingReviewDialog = () => {
+ // hasContactSpoofingReview is always false in stories
+ return
;
};
const getAbout = () => '👍 Free to chat';
@@ -433,6 +406,7 @@ const renderHeroRow = () => {
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={noop}
viewUserStories={action('viewUserStories')}
+ toggleAboutContactModal={action('toggleAboutContactModal')}
/>
);
}
@@ -452,6 +426,9 @@ const renderTypingBubble = () => (
theme={ThemeType.light}
/>
);
+const renderCollidingAvatars = () => (
+
+);
const renderMiniPlayer = () => (
If active, this is where smart mini player would be
);
@@ -477,12 +454,14 @@ const useProps = (overrideProps: Partial
= {}): PropsType => ({
invitedContactsForNewlyCreatedGroup:
overrideProps.invitedContactsForNewlyCreatedGroup || [],
warning: overrideProps.warning,
+ hasContactSpoofingReview: false,
id: uuid(),
renderItem,
renderHeroRow,
renderMiniPlayer,
renderTypingBubble,
+ renderCollidingAvatars,
renderContactSpoofingReviewDialog,
isSomeoneTyping: overrideProps.isSomeoneTyping || false,
@@ -581,7 +560,9 @@ export function WithSameNameInDirectConversationWarning(): JSX.Element {
const props = useProps({
warning: {
type: ContactSpoofingType.DirectConversationWithSameTitle,
- safeConversation: getDefaultConversation(),
+
+ // Just to pacify type-script
+ safeConversationId: '123',
},
items: [],
});
@@ -590,6 +571,21 @@ export function WithSameNameInDirectConversationWarning(): JSX.Element {
}
export function WithSameNameInGroupConversationWarning(): JSX.Element {
+ const props = useProps({
+ warning: {
+ type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
+ acknowledgedGroupNameCollisions: {},
+ groupNameCollisions: {
+ Alice: times(2, () => uuid()),
+ },
+ },
+ items: [],
+ });
+
+ return ;
+}
+
+export function WithSameNamesInGroupConversationWarning(): JSX.Element {
const props = useProps({
warning: {
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx
index 8509dcab30..d0717eefcc 100644
--- a/ts/components/conversation/Timeline.tsx
+++ b/ts/components/conversation/Timeline.tsx
@@ -58,7 +58,7 @@ const LOAD_NEWER_THRESHOLD = 5;
export type WarningType = ReadonlyDeep<
| {
type: ContactSpoofingType.DirectConversationWithSameTitle;
- safeConversation: ConversationType;
+ safeConversationId: string;
}
| {
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
@@ -67,23 +67,6 @@ export type WarningType = ReadonlyDeep<
}
>;
-export type ContactSpoofingReviewPropType =
- | {
- type: ContactSpoofingType.DirectConversationWithSameTitle;
- possiblyUnsafeConversation: ConversationType;
- safeConversation: ConversationType;
- }
- | {
- type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
- collisionInfoByTitle: Record<
- string,
- Array<{
- oldName?: string;
- conversation: ConversationType;
- }>
- >;
- };
-
export type PropsDataType = {
haveNewest: boolean;
haveOldest: boolean;
@@ -112,7 +95,7 @@ type PropsHousekeepingType = {
shouldShowMiniPlayer: boolean;
warning?: WarningType;
- contactSpoofingReview?: ContactSpoofingReviewPropType;
+ hasContactSpoofingReview: boolean | undefined;
discardMessages: (
_: Readonly<
@@ -128,6 +111,9 @@ type PropsHousekeepingType = {
i18n: LocalizerType;
theme: ThemeType;
+ renderCollidingAvatars: (_: {
+ conversationIds: ReadonlyArray;
+ }) => JSX.Element;
renderContactSpoofingReviewDialog: (
props: SmartContactSpoofingReviewDialogPropsType
) => JSX.Element;
@@ -167,12 +153,7 @@ export type PropsActionsType = {
setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown;
peekGroupCallForTheFirstTime: (conversationId: string) => unknown;
peekGroupCallIfItHasMembers: (conversationId: string) => unknown;
- reviewGroupMemberNameCollision: (groupConversationId: string) => void;
- reviewMessageRequestNameCollision: (
- _: Readonly<{
- safeConversationId: string;
- }>
- ) => void;
+ reviewConversationNameCollision: () => void;
scrollToOldestUnreadMention: (conversationId: string) => unknown;
};
@@ -798,7 +779,7 @@ export class Timeline extends React.Component<
acknowledgeGroupMemberNameCollisions,
clearInvitedServiceIdsForNewlyCreatedGroup,
closeContactSpoofingReview,
- contactSpoofingReview,
+ hasContactSpoofingReview,
getPreferredBadge,
getTimestampForMessage,
haveNewest,
@@ -811,13 +792,13 @@ export class Timeline extends React.Component<
items,
messageLoadingState,
oldestUnseenIndex,
+ renderCollidingAvatars,
renderContactSpoofingReviewDialog,
renderHeroRow,
renderItem,
renderMiniPlayer,
renderTypingBubble,
- reviewGroupMemberNameCollision,
- reviewMessageRequestNameCollision,
+ reviewConversationNameCollision,
scrollToOldestUnreadMention,
shouldShowMiniPlayer,
theme,
@@ -963,8 +944,14 @@ export class Timeline extends React.Component<
let headerElements: ReactNode;
if (warning || shouldShowMiniPlayer) {
let text: ReactChild | undefined;
+ let icon: ReactChild | undefined;
let onClose: () => void;
if (warning) {
+ icon = (
+
+
+
+ );
switch (warning.type) {
case ContactSpoofingType.DirectConversationWithSameTitle:
text = (
@@ -976,11 +963,7 @@ export class Timeline extends React.Component<
// eslint-disable-next-line react/no-unstable-nested-components
reviewRequestLink: parts => (
{
- reviewMessageRequestNameCollision({
- safeConversationId: warning.safeConversation.id,
- });
- }}
+ onClick={reviewConversationNameCollision}
>
{parts}
@@ -998,24 +981,25 @@ export class Timeline extends React.Component<
const { groupNameCollisions } = warning;
const numberOfSharedNames = Object.keys(groupNameCollisions).length;
const reviewRequestLink: FullJSXType = parts => (
- {
- reviewGroupMemberNameCollision(id);
- }}
- >
+
{parts}
);
if (numberOfSharedNames === 1) {
+ const [conversationIds] = [...Object.values(groupNameCollisions)];
+ if (conversationIds.length >= 2) {
+ icon = (
+
+ {renderCollidingAvatars({ conversationIds })}
+
+ );
+ }
text = (
result + conversations.length,
- 0
- ),
+ count: conversationIds.length,
reviewRequestLink,
}}
/>
@@ -1053,9 +1037,7 @@ export class Timeline extends React.Component<
{renderMiniPlayer({ shouldFlow: true })}
{text && (
-
-
-
+ {icon}
{text}
)}
@@ -1066,33 +1048,11 @@ export class Timeline extends React.Component<
}
let contactSpoofingReviewDialog: ReactNode;
- if (contactSpoofingReview) {
- const commonProps = {
+ if (hasContactSpoofingReview) {
+ contactSpoofingReviewDialog = renderContactSpoofingReviewDialog({
conversationId: id,
onClose: closeContactSpoofingReview,
- };
-
- switch (contactSpoofingReview.type) {
- case ContactSpoofingType.DirectConversationWithSameTitle:
- contactSpoofingReviewDialog = renderContactSpoofingReviewDialog({
- ...commonProps,
- type: ContactSpoofingType.DirectConversationWithSameTitle,
- possiblyUnsafeConversation:
- contactSpoofingReview.possiblyUnsafeConversation,
- safeConversation: contactSpoofingReview.safeConversation,
- });
- break;
- case ContactSpoofingType.MultipleGroupMembersWithSameTitle:
- contactSpoofingReviewDialog = renderContactSpoofingReviewDialog({
- ...commonProps,
- type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
- groupConversationId: id,
- collisionInfoByTitle: contactSpoofingReview.collisionInfoByTitle,
- });
- break;
- default:
- throw missingCaseError(contactSpoofingReview);
- }
+ });
}
return (
diff --git a/ts/components/conversation/TimelineWarning.tsx b/ts/components/conversation/TimelineWarning.tsx
index c66c2b4220..299243a5cc 100644
--- a/ts/components/conversation/TimelineWarning.tsx
+++ b/ts/components/conversation/TimelineWarning.tsx
@@ -71,3 +71,11 @@ function Link({ children, onClick }: Readonly): JSX.Element {
}
TimelineWarning.Link = Link;
+
+function CustomInfo({
+ children,
+}: Readonly<{ children: ReactNode }>): JSX.Element {
+ return {children}
;
+}
+
+TimelineWarning.CustomInfo = CustomInfo;
diff --git a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx
index 002454fdb2..74043e1882 100644
--- a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx
+++ b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx
@@ -102,6 +102,7 @@ const createProps = (
setMuteExpiration: action('setMuteExpiration'),
userAvatarData: [],
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
+ toggleAboutContactModal: action('toggleAboutContactModal'),
toggleAddUserToAnotherGroupModal: action('toggleAddUserToAnotherGroup'),
onOutgoingAudioCallInConversation: action(
'onOutgoingAudioCallInConversation'
diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx
index 95e8997c62..22e4d45650 100644
--- a/ts/components/conversation/conversation-details/ConversationDetails.tsx
+++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx
@@ -150,6 +150,7 @@ type ActionProps = {
setMuteExpiration: (id: string, muteExpiresAt: undefined | number) => unknown;
showContactModal: (contactId: string, conversationId?: string) => void;
showConversation: ShowConversationType;
+ toggleAboutContactModal: (contactId: string) => void;
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
toggleSafetyNumberModal: (conversationId: string) => unknown;
updateGroupAttributes: (
@@ -223,6 +224,7 @@ export function ConversationDetails({
showConversation,
showLightboxWithMedia,
theme,
+ toggleAboutContactModal,
toggleSafetyNumberModal,
toggleAddUserToAnotherGroupModal,
updateGroupAttributes,
@@ -398,6 +400,7 @@ export function ConversationDetails({
);
}}
theme={theme}
+ toggleAboutContactModal={toggleAboutContactModal}
/>
diff --git a/ts/components/conversation/conversation-details/ConversationDetailsHeader.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetailsHeader.stories.tsx
index 606fd20247..a73c8a75e1 100644
--- a/ts/components/conversation/conversation-details/ConversationDetailsHeader.stories.tsx
+++ b/ts/components/conversation/conversation-details/ConversationDetailsHeader.stories.tsx
@@ -45,6 +45,7 @@ function Wrapper(overrideProps: Partial
) {
isGroup
isMe={false}
theme={theme}
+ toggleAboutContactModal={action('toggleAboutContactModal')}
{...overrideProps}
/>
);
@@ -80,7 +81,16 @@ export function EditableNoDescription(): JSX.Element {
}
export function OneOnOne(): JSX.Element {
- return ;
+ return (
+
+ );
}
export function NoteToSelf(): JSX.Element {
diff --git a/ts/components/conversation/conversation-details/ConversationDetailsHeader.tsx b/ts/components/conversation/conversation-details/ConversationDetailsHeader.tsx
index 1b63701a21..6788f9b051 100644
--- a/ts/components/conversation/conversation-details/ConversationDetailsHeader.tsx
+++ b/ts/components/conversation/conversation-details/ConversationDetailsHeader.tsx
@@ -11,7 +11,7 @@ import { GroupDescription } from '../GroupDescription';
import { About } from '../About';
import type { GroupV2Membership } from './ConversationDetailsMembershipList';
import type { LocalizerType, ThemeType } from '../../../types/Util';
-import { bemGenerator } from './util';
+import { assertDev } from '../../../util/assert';
import { BadgeDialog } from '../../BadgeDialog';
import type { BadgeType } from '../../../badges/types';
import { UserText } from '../../UserText';
@@ -26,6 +26,7 @@ export type Props = {
isMe: boolean;
memberships: ReadonlyArray;
startEditing: (isGroupTitle: boolean) => void;
+ toggleAboutContactModal: (contactId: string) => void;
theme: ThemeType;
};
@@ -34,8 +35,6 @@ enum ConversationDetailsHeaderActiveModal {
ShowingBadges,
}
-const bem = bemGenerator('ConversationDetails-header');
-
export function ConversationDetailsHeader({
areWeASubscriber,
badges,
@@ -46,6 +45,7 @@ export function ConversationDetailsHeader({
isMe,
memberships,
startEditing,
+ toggleAboutContactModal,
theme,
}: Props): JSX.Element {
const [activeModal, setActiveModal] = useState<
@@ -75,10 +75,10 @@ export function ConversationDetailsHeader({
} else if (!isMe) {
subtitle = (
<>
-
+
-
+
{conversation.phoneNumber}
>
@@ -105,15 +105,6 @@ export function ConversationDetailsHeader({
/>
);
- const contents = (
-
-
- {isMe ? i18n('icu:noteToSelf') : }
- {isMe && }
-
-
- );
-
let modal: ReactNode;
switch (activeModal) {
case ConversationDetailsHeaderActiveModal.ShowingAvatar:
@@ -150,8 +141,13 @@ export function ConversationDetailsHeader({
}
if (canEdit) {
+ assertDev(isGroup, 'Only groups support editable title');
+
return (
-
+
{modal}
{avatar}
- {contents}
+
+
+
{hasNestedButton ? (
-
{subtitle}
+
{subtitle}
) : (
- {subtitle}
+
+ {subtitle}
+
)}
);
}
+ let title: JSX.Element;
+
+ if (isMe) {
+ title = (
+
+ {i18n('icu:noteToSelf')}
+
+
+ );
+ } else if (isGroup) {
+ title = (
+
+
+
+ );
+ } else {
+ title = (
+
{
+ ev.preventDefault();
+ ev.stopPropagation();
+ toggleAboutContactModal(conversation.id);
+ }}
+ className="ConversationDetailsHeader__about-button"
+ >
+
+
+
+
+
+
+ );
+ }
+
return (
-
+
{modal}
{avatar}
- {contents}
-
{subtitle}
+ {title}
+
{subtitle}
);
}
diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts
index cb931935f3..4001685f56 100644
--- a/ts/model-types.d.ts
+++ b/ts/model-types.d.ts
@@ -381,6 +381,7 @@ export type ConversationAttributesType = {
profileKey?: string;
profileName?: string;
verified?: number;
+ profileLastUpdatedAt?: number;
profileLastFetchedAt?: number;
pendingUniversalTimer?: string;
pendingRemovedContactNotification?: string;
diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts
index 37e55ad4bb..d6f6582350 100644
--- a/ts/models/conversations.ts
+++ b/ts/models/conversations.ts
@@ -3185,6 +3185,8 @@ export class ConversationModel extends window.Backbone
const serviceId = this.getServiceId();
if (isDirectConversation(this.attributes) && serviceId) {
+ this.set({ profileLastUpdatedAt: Date.now() });
+
void window.ConversationController.getAllGroupsInvolvingServiceId(
serviceId
).then(groups => {
diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts
index 4927281a3e..f94dfcce53 100644
--- a/ts/state/ducks/conversations.ts
+++ b/ts/state/ducks/conversations.ts
@@ -83,7 +83,6 @@ import {
import { isMessageUnread } from '../../util/isMessageUnread';
import { toggleSelectedContactForGroupAddition } from '../../groups/toggleSelectedContactForGroupAddition';
import type { GroupNameCollisionsWithIdsByTitle } from '../../util/groupMemberNameCollisions';
-import { ContactSpoofingType } from '../../util/contactSpoofing';
import { writeProfile } from '../../services/writeProfile';
import {
getConversationServiceIdsStoppingSend,
@@ -237,6 +236,7 @@ export type ConversationType = ReadonlyDeep<
familyName?: string;
firstName?: string;
profileName?: string;
+ profileLastUpdatedAt?: number;
username?: string;
about?: string;
aboutText?: string;
@@ -464,17 +464,6 @@ type ComposerStateType = ReadonlyDeep<
))
>;
-type ContactSpoofingReviewStateType = ReadonlyDeep<
- | {
- type: ContactSpoofingType.DirectConversationWithSameTitle;
- safeConversationId: string;
- }
- | {
- type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
- groupConversationId: string;
- }
->;
-
// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME
export type ConversationsStateType = Readonly<{
preJoinConversation?: PreJoinConversationType;
@@ -502,7 +491,7 @@ export type ConversationsStateType = Readonly<{
showArchived: boolean;
composer?: ComposerStateType;
- contactSpoofingReview?: ContactSpoofingReviewStateType;
+ hasContactSpoofingReview: boolean;
/**
* Each key is a conversation ID. Each value is a value representing the state of
@@ -850,17 +839,8 @@ export type TargetedConversationChangedActionType = ReadonlyDeep<{
switchToAssociatedView?: boolean;
};
}>;
-type ReviewGroupMemberNameCollisionActionType = ReadonlyDeep<{
- type: 'REVIEW_GROUP_MEMBER_NAME_COLLISION';
- payload: {
- groupConversationId: string;
- };
-}>;
-type ReviewMessageRequestNameCollisionActionType = ReadonlyDeep<{
- type: 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION';
- payload: {
- safeConversationId: string;
- };
+type ReviewConversationNameCollisionActionType = ReadonlyDeep<{
+ type: 'REVIEW_CONVERSATION_NAME_COLLISION';
}>;
type ShowInboxActionType = ReadonlyDeep<{
type: 'SHOW_INBOX';
@@ -989,8 +969,7 @@ export type ConversationActionType =
| RepairNewestMessageActionType
| RepairOldestMessageActionType
| ReplaceAvatarsActionType
- | ReviewGroupMemberNameCollisionActionType
- | ReviewMessageRequestNameCollisionActionType
+ | ReviewConversationNameCollisionActionType
| ScrollToMessageActionType
| TargetedConversationChangedActionType
| SetComposeGroupAvatarActionType
@@ -1092,8 +1071,7 @@ export const actions = {
copyMessageText,
retryDeleteForEveryone,
retryMessageSend,
- reviewGroupMemberNameCollision,
- reviewMessageRequestNameCollision,
+ reviewConversationNameCollision,
revokePendingMembershipsFromGroupV2,
saveAttachment,
saveAttachmentFromMessage,
@@ -2885,23 +2863,12 @@ function repairOldestMessage(
};
}
-function reviewGroupMemberNameCollision(
- groupConversationId: string
-): ReviewGroupMemberNameCollisionActionType {
+function reviewConversationNameCollision(): ReviewConversationNameCollisionActionType {
return {
- type: 'REVIEW_GROUP_MEMBER_NAME_COLLISION',
- payload: { groupConversationId },
+ type: 'REVIEW_CONVERSATION_NAME_COLLISION',
};
}
-function reviewMessageRequestNameCollision(
- payload: Readonly<{
- safeConversationId: string;
- }>
-): ReviewMessageRequestNameCollisionActionType {
- return { type: 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION', payload };
-}
-
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type MessageResetOptionsType = {
conversationId: string;
@@ -4208,6 +4175,7 @@ export function getEmptyState(): ConversationsStateType {
lastSelectedMessage: undefined,
selectedMessageIds: undefined,
showArchived: false,
+ hasContactSpoofingReview: false,
targetedConversationPanels: {
isAnimating: false,
wasAnimated: false,
@@ -4591,7 +4559,10 @@ export function reducer(
}
if (action.type === 'CLOSE_CONTACT_SPOOFING_REVIEW') {
- return omit(state, 'contactSpoofingReview');
+ return {
+ ...state,
+ hasContactSpoofingReview: false,
+ };
}
if (action.type === 'CLOSE_MAXIMUM_GROUP_SIZE_MODAL') {
@@ -4713,6 +4684,7 @@ export function reducer(
}
const keysToOmit: Array
= [];
+ const keyValuesToAdd: { hasContactSpoofingReview?: false } = {};
if (selectedConversationId === id) {
// Archived -> Inbox: we go back to the normal inbox view
@@ -4728,12 +4700,13 @@ export function reducer(
}
if (!existing.isBlocked && data.isBlocked) {
- keysToOmit.push('contactSpoofingReview');
+ keyValuesToAdd.hasContactSpoofingReview = false;
}
}
return {
...omit(state, keysToOmit),
+ ...keyValuesToAdd,
selectedConversationId,
showArchived,
conversationLookup: {
@@ -4775,7 +4748,8 @@ export function reducer(
: undefined;
return {
- ...omit(state, 'contactSpoofingReview'),
+ ...state,
+ hasContactSpoofingReview: false,
selectedConversationId,
targetedConversationPanels: {
isAnimating: false,
@@ -5494,23 +5468,10 @@ export function reducer(
};
}
- if (action.type === 'REVIEW_GROUP_MEMBER_NAME_COLLISION') {
+ if (action.type === 'REVIEW_CONVERSATION_NAME_COLLISION') {
return {
...state,
- contactSpoofingReview: {
- type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
- ...action.payload,
- },
- };
- }
-
- if (action.type === 'REVIEW_MESSAGE_REQUEST_NAME_COLLISION') {
- return {
- ...state,
- contactSpoofingReview: {
- type: ContactSpoofingType.DirectConversationWithSameTitle,
- ...action.payload,
- },
+ hasContactSpoofingReview: true,
};
}
@@ -5683,7 +5644,8 @@ export function reducer(
}
const nextState = {
- ...omit(state, 'contactSpoofingReview'),
+ ...state,
+ hasContactSpoofingReview: false,
selectedConversationId: conversationId,
targetedMessage: messageId,
targetedMessageSource: TargetedMessageSource.NavigateToMessage,
diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts
index aa1b18e28b..db3092e493 100644
--- a/ts/state/ducks/globalModals.ts
+++ b/ts/state/ducks/globalModals.ts
@@ -77,9 +77,13 @@ type MigrateToGV2PropsType = ReadonlyDeep<{
hasMigrated: boolean;
invitedMemberIds: Array;
}>;
+export type AboutContactModalPropsType = ReadonlyDeep<{
+ contactId: string;
+}>;
export type GlobalModalsStateType = ReadonlyDeep<{
addUserToAnotherGroupModalContactId?: string;
+ aboutContactModalProps?: AboutContactModalPropsType;
authArtCreatorData?: AuthorizeArtCreatorDataType;
contactModalState?: ContactModalStateType;
deleteMessagesProps?: DeleteMessagesPropsType;
@@ -130,6 +134,7 @@ export const TOGGLE_PROFILE_EDITOR_ERROR =
const TOGGLE_SAFETY_NUMBER_MODAL = 'globalModals/TOGGLE_SAFETY_NUMBER_MODAL';
const TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL =
'globalModals/TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL';
+const TOGGLE_ABOUT_MODAL = 'globalModals/TOGGLE_ABOUT_MODAL';
const TOGGLE_SIGNAL_CONNECTIONS_MODAL =
'globalModals/TOGGLE_SIGNAL_CONNECTIONS_MODAL';
export const SHOW_SEND_ANYWAY_DIALOG = 'globalModals/SHOW_SEND_ANYWAY_DIALOG';
@@ -230,6 +235,11 @@ type ToggleAddUserToAnotherGroupModalActionType = ReadonlyDeep<{
payload: string | undefined;
}>;
+type ToggleAboutContactModalActionType = ReadonlyDeep<{
+ type: typeof TOGGLE_ABOUT_MODAL;
+ payload: AboutContactModalPropsType | undefined;
+}>;
+
type ToggleSignalConnectionsModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_SIGNAL_CONNECTIONS_MODAL;
}>;
@@ -372,6 +382,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| ShowUserNotFoundModalActionType
| ShowWhatsNewModalActionType
| StartMigrationToGV2ActionType
+ | ToggleAboutContactModalActionType
| ToggleAddUserToAnotherGroupModalActionType
| ToggleConfirmationModalActionType
| ToggleDeleteMessagesModalActionType
@@ -411,6 +422,7 @@ export const actions = {
showStoriesSettings,
showUserNotFoundModal,
showWhatsNewModal,
+ toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
toggleConfirmationModal,
toggleDeleteMessagesModal,
@@ -627,6 +639,15 @@ function toggleAddUserToAnotherGroupModal(
};
}
+function toggleAboutContactModal(
+ contactId?: string
+): ToggleAboutContactModalActionType {
+ return {
+ type: TOGGLE_ABOUT_MODAL,
+ payload: contactId ? { contactId } : undefined,
+ };
+}
+
function toggleSignalConnectionsModal(): ToggleSignalConnectionsModalActionType {
return {
type: TOGGLE_SIGNAL_CONNECTIONS_MODAL,
@@ -891,6 +912,13 @@ export function reducer(
state: Readonly = getEmptyState(),
action: Readonly
): GlobalModalsStateType {
+ if (action.type === TOGGLE_ABOUT_MODAL) {
+ return {
+ ...state,
+ aboutContactModalProps: action.payload,
+ };
+ }
+
if (action.type === TOGGLE_PROFILE_EDITOR) {
return {
...state,
diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts
index 2ca7328578..add175017f 100644
--- a/ts/state/selectors/conversations.ts
+++ b/ts/state/selectors/conversations.ts
@@ -154,11 +154,34 @@ export const getAllSignalConnections = createSelector(
conversations.filter(isSignalConnection)
);
-export const getConversationsByTitleSelector = createSelector(
+export const getSafeConversationWithSameTitle = createSelector(
getAllConversations,
- (conversations): ((title: string) => Array) =>
- (title: string) =>
- conversations.filter(conversation => conversation.title === title)
+ (
+ _state: StateType,
+ {
+ possiblyUnsafeConversation,
+ }: {
+ possiblyUnsafeConversation: ConversationType;
+ }
+ ) => possiblyUnsafeConversation,
+ (conversations, possiblyUnsafeConversation): ConversationType | undefined => {
+ const conversationsWithSameTitle = conversations.filter(conversation => {
+ return conversation.title === possiblyUnsafeConversation.title;
+ });
+ assertDev(
+ conversationsWithSameTitle.length,
+ 'Expected at least 1 conversation with the same title (this one)'
+ );
+
+ const safeConversation = conversationsWithSameTitle.find(
+ otherConversation =>
+ otherConversation.acceptedMessageRequest &&
+ otherConversation.type === 'direct' &&
+ otherConversation.id !== possiblyUnsafeConversation.id
+ );
+
+ return safeConversation;
+ }
);
export const getSelectedConversationId = createSelector(
diff --git a/ts/state/smart/CollidingAvatars.tsx b/ts/state/smart/CollidingAvatars.tsx
new file mode 100644
index 0000000000..483886111e
--- /dev/null
+++ b/ts/state/smart/CollidingAvatars.tsx
@@ -0,0 +1,28 @@
+// Copyright 2024 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import React, { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+
+import { CollidingAvatars } from '../../components/CollidingAvatars';
+import { getIntl } from '../selectors/user';
+import { getConversationSelector } from '../selectors/conversations';
+
+export type PropsType = Readonly<{
+ conversationIds: ReadonlyArray;
+}>;
+
+export function SmartCollidingAvatars({
+ conversationIds,
+}: PropsType): JSX.Element {
+ const i18n = useSelector(getIntl);
+ const getConversation = useSelector(getConversationSelector);
+
+ const conversations = useMemo(() => {
+ return conversationIds.map(getConversation).sort((a, b) => {
+ return (b.profileLastUpdatedAt ?? 0) - (a.profileLastUpdatedAt ?? 0);
+ });
+ }, [conversationIds, getConversation]);
+
+ return ;
+}
diff --git a/ts/state/smart/ContactSpoofingReviewDialog.tsx b/ts/state/smart/ContactSpoofingReviewDialog.tsx
index c7c46a96e3..39e7283f69 100644
--- a/ts/state/smart/ContactSpoofingReviewDialog.tsx
+++ b/ts/state/smart/ContactSpoofingReviewDialog.tsx
@@ -1,48 +1,42 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
-import * as React from 'react';
+import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
+import { mapValues } from 'lodash';
import type { StateType } from '../reducer';
import { ContactSpoofingReviewDialog } from '../../components/conversation/ContactSpoofingReviewDialog';
-import type { ConversationType } from '../ducks/conversations';
import { useConversationsActions } from '../ducks/conversations';
import type { GetConversationByIdType } from '../selectors/conversations';
-import { getConversationSelector } from '../selectors/conversations';
+import {
+ getConversationSelector,
+ getConversationByServiceIdSelector,
+ getSafeConversationWithSameTitle,
+} from '../selectors/conversations';
+import { getOwn } from '../../util/getOwn';
+import { assertDev } from '../../util/assert';
import { ContactSpoofingType } from '../../util/contactSpoofing';
+import { getGroupMemberships } from '../../util/getGroupMemberships';
+import { isSignalConnection } from '../../util/getSignalConnections';
+import {
+ getCollisionsFromMemberships,
+ invertIdsByTitle,
+} from '../../util/groupMemberNameCollisions';
import { useGlobalModalActions } from '../ducks/globalModals';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user';
-export type PropsType =
- | {
- conversationId: string;
- onClose: () => void;
- } & (
- | {
- type: ContactSpoofingType.DirectConversationWithSameTitle;
- possiblyUnsafeConversation: ConversationType;
- safeConversation: ConversationType;
- }
- | {
- type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
- groupConversationId: string;
- collisionInfoByTitle: Record<
- string,
- Array<{
- oldName?: string;
- conversation: ConversationType;
- }>
- >;
- }
- );
+export type PropsType = Readonly<{
+ conversationId: string;
+ onClose: () => void;
+}>;
export function SmartContactSpoofingReviewDialog(
props: PropsType
-): JSX.Element {
- const { type } = props;
+): JSX.Element | null {
+ const { conversationId } = props;
const getConversation = useSelector(
getConversationSelector
@@ -55,12 +49,29 @@ export function SmartContactSpoofingReviewDialog(
deleteConversation,
removeMember,
} = useConversationsActions();
- const { showContactModal } = useGlobalModalActions();
+ const { showContactModal, toggleSignalConnectionsModal } =
+ useGlobalModalActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
+ const getConversationByServiceId = useSelector(
+ getConversationByServiceIdSelector
+ );
+ const conversation = getConversation(conversationId);
+
+ // Just binding the options argument
+ const safeConversationSelector = useCallback(
+ (state: StateType) => {
+ return getSafeConversationWithSameTitle(state, {
+ possiblyUnsafeConversation: conversation,
+ });
+ },
+ [conversation]
+ );
+ const safeConvo = useSelector(safeConversationSelector);
const sharedProps = {
+ ...props,
acceptConversation,
blockAndReportSpam,
blockConversation,
@@ -69,18 +80,65 @@ export function SmartContactSpoofingReviewDialog(
i18n,
removeMember,
showContactModal,
+ toggleSignalConnectionsModal,
theme,
};
- if (type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) {
+ if (conversation.type === 'group') {
+ const { memberships } = getGroupMemberships(
+ conversation,
+ getConversationByServiceId
+ );
+ const groupNameCollisions = getCollisionsFromMemberships(memberships);
+
+ const previouslyAcknowledgedTitlesById = invertIdsByTitle(
+ conversation.acknowledgedGroupNameCollisions
+ );
+
+ const collisionInfoByTitle = mapValues(groupNameCollisions, collisions =>
+ collisions.map(collision => ({
+ conversation: collision,
+ isSignalConnection: isSignalConnection(collision),
+ oldName: getOwn(previouslyAcknowledgedTitlesById, collision.id),
+ }))
+ );
+
return (
);
}
- return ;
+ const possiblyUnsafeConvo = conversation;
+ assertDev(
+ possiblyUnsafeConvo.type === 'direct',
+ 'DirectConversationWithSameTitle: expects possibly unsafe direct ' +
+ 'conversation'
+ );
+
+ if (!safeConvo) {
+ return null;
+ }
+
+ const possiblyUnsafe = {
+ conversation: possiblyUnsafeConvo,
+ isSignalConnection: isSignalConnection(possiblyUnsafeConvo),
+ };
+ const safe = {
+ conversation: safeConvo,
+ isSignalConnection: isSignalConnection(safeConvo),
+ };
+
+ return (
+
+ );
}
diff --git a/ts/state/smart/GlobalModalContainer.tsx b/ts/state/smart/GlobalModalContainer.tsx
index 21ac55c122..3458047e31 100644
--- a/ts/state/smart/GlobalModalContainer.tsx
+++ b/ts/state/smart/GlobalModalContainer.tsx
@@ -6,6 +6,8 @@ import { useSelector } from 'react-redux';
import type { GlobalModalsStateType } from '../ducks/globalModals';
import type { StateType } from '../reducer';
+import { isSignalConnection } from '../../util/getSignalConnections';
+import type { ExternalPropsType as AboutContactModalPropsType } from '../../components/conversation/AboutContactModal';
import { ErrorModal } from '../../components/ErrorModal';
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
import { SmartAddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal';
@@ -19,9 +21,13 @@ import { SmartSendAnywayDialog } from './SendAnywayDialog';
import { SmartShortcutGuideModal } from './ShortcutGuideModal';
import { SmartStickerPreviewModal } from './StickerPreviewModal';
import { SmartStoriesSettingsModal } from './StoriesSettingsModal';
-import { getConversationsStoppingSend } from '../selectors/conversations';
+import {
+ getConversationSelector,
+ getConversationsStoppingSend,
+} from '../selectors/conversations';
import { getIntl, getTheme } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
+import { useConversationsActions } from '../ducks/conversations';
import { SmartDeleteMessagesModal } from './DeleteMessagesModal';
function renderEditHistoryMessagesModal(): JSX.Element {
@@ -62,12 +68,14 @@ function renderShortcutGuideModal(): JSX.Element {
export function SmartGlobalModalContainer(): JSX.Element {
const conversationsStoppingSend = useSelector(getConversationsStoppingSend);
+ const getConversation = useSelector(getConversationSelector);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const hasSafetyNumberChangeModal = conversationsStoppingSend.length > 0;
const {
+ aboutContactModalProps: aboutContactModalRawProps,
addUserToAnotherGroupModalContactId,
authArtCreatorData,
contactModalState,
@@ -100,9 +108,24 @@ export function SmartGlobalModalContainer(): JSX.Element {
hideWhatsNewModal,
showFormattingWarningModal,
showSendEditWarningModal,
+ toggleAboutContactModal,
toggleSignalConnectionsModal,
} = useGlobalModalActions();
+ const { updateSharedGroups } = useConversationsActions();
+
+ let aboutContactModalProps: AboutContactModalPropsType | undefined;
+ if (aboutContactModalRawProps) {
+ const conversation = getConversation(aboutContactModalRawProps.contactId);
+
+ aboutContactModalProps = {
+ conversation,
+ isSignalConnection: isSignalConnection(conversation),
+ toggleSignalConnectionsModal,
+ updateSharedGroups,
+ };
+ }
+
const renderAddUserToAnotherGroup = useCallback(() => {
return (
;
+}
+
function renderContactSpoofingReviewDialog(
props: SmartContactSpoofingReviewDialogPropsType
): JSX.Element {
@@ -109,27 +111,14 @@ const getWarning = (
switch (conversation.type) {
case 'direct':
if (!conversation.acceptedMessageRequest && !conversation.isBlocked) {
- const getConversationsWithTitle =
- getConversationsByTitleSelector(state);
- const conversationsWithSameTitle = getConversationsWithTitle(
- conversation.title
- );
- assertDev(
- conversationsWithSameTitle.length,
- 'Expected at least 1 conversation with the same title (this one)'
- );
-
- const safeConversation = conversationsWithSameTitle.find(
- otherConversation =>
- otherConversation.acceptedMessageRequest &&
- otherConversation.type === 'direct' &&
- otherConversation.id !== conversation.id
- );
+ const safeConversation = getSafeConversationWithSameTitle(state, {
+ possiblyUnsafeConversation: conversation,
+ });
if (safeConversation) {
return {
type: ContactSpoofingType.DirectConversationWithSameTitle,
- safeConversation,
+ safeConversationId: safeConversation.id,
};
}
}
@@ -165,63 +154,6 @@ const getWarning = (
}
};
-const getContactSpoofingReview = (
- selectedConversationId: string,
- state: Readonly
-): undefined | ContactSpoofingReviewPropType => {
- const { contactSpoofingReview } = state.conversations;
- if (!contactSpoofingReview) {
- return undefined;
- }
-
- const conversationSelector = getConversationSelector(state);
- const getConversationByServiceId = getConversationByServiceIdSelector(state);
-
- const currentConversation = conversationSelector(selectedConversationId);
-
- switch (contactSpoofingReview.type) {
- case ContactSpoofingType.DirectConversationWithSameTitle:
- return {
- type: ContactSpoofingType.DirectConversationWithSameTitle,
- possiblyUnsafeConversation: currentConversation,
- safeConversation: conversationSelector(
- contactSpoofingReview.safeConversationId
- ),
- };
- case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
- assertDev(
- currentConversation.type === 'group',
- 'MultipleGroupMembersWithSameTitle: expects group conversation'
- );
- const { memberships } = getGroupMemberships(
- currentConversation,
- getConversationByServiceId
- );
- const groupNameCollisions = getCollisionsFromMemberships(memberships);
-
- const previouslyAcknowledgedTitlesById = invertIdsByTitle(
- currentConversation.acknowledgedGroupNameCollisions
- );
-
- const collisionInfoByTitle = mapValues(
- groupNameCollisions,
- conversations =>
- conversations.map(conversation => ({
- conversation,
- oldName: getOwn(previouslyAcknowledgedTitlesById, conversation.id),
- }))
- );
-
- return {
- type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
- collisionInfoByTitle,
- };
- }
- default:
- throw missingCaseError(contactSpoofingReview);
- }
-};
-
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id } = props;
@@ -259,13 +191,14 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
shouldShowMiniPlayer,
warning: getWarning(conversation, state),
- contactSpoofingReview: getContactSpoofingReview(id, state),
+ hasContactSpoofingReview: state.conversations.hasContactSpoofingReview,
getTimestampForMessage,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
theme: getTheme(state),
+ renderCollidingAvatars,
renderContactSpoofingReviewDialog,
renderHeroRow,
renderItem,
diff --git a/ts/test-both/state/selectors/conversations_test.ts b/ts/test-both/state/selectors/conversations_test.ts
index 1ab71eaabc..af3d245ede 100644
--- a/ts/test-both/state/selectors/conversations_test.ts
+++ b/ts/test-both/state/selectors/conversations_test.ts
@@ -29,7 +29,7 @@ import {
getContactNameColorSelector,
getConversationByIdSelector,
getConversationServiceIdsStoppingSend,
- getConversationsByTitleSelector,
+ getSafeConversationWithSameTitle,
getConversationSelector,
getConversationsStoppingSend,
getFilteredCandidateContactsForNewGroup,
@@ -1577,32 +1577,32 @@ describe('both/state/selectors/conversations-extra', () => {
});
});
- describe('#getConversationsByTitleSelector', () => {
+ describe('#getSafeConversationWithSameTitle', () => {
it('returns a selector that finds conversations by title', () => {
+ const unsafe = { ...makeConversation('abc'), title: 'Janet' };
+ const safe = { ...makeConversation('def'), title: 'Janet' };
+ const unique = { ...makeConversation('geh'), title: 'Rick' };
const state = {
...getEmptyRootState(),
conversations: {
...getEmptyState(),
conversationLookup: {
- abc: { ...makeConversation('abc'), title: 'Janet' },
- def: { ...makeConversation('def'), title: 'Janet' },
- geh: { ...makeConversation('geh'), title: 'Rick' },
+ abc: unsafe,
+ def: safe,
+ geh: unique,
},
},
};
- const selector = getConversationsByTitleSelector(state);
+ const janet = getSafeConversationWithSameTitle(state, {
+ possiblyUnsafeConversation: unsafe,
+ });
+ assert.strictEqual(janet, safe);
- assert.sameMembers(
- selector('Janet').map(c => c.id),
- ['abc', 'def']
- );
- assert.sameMembers(
- selector('Rick').map(c => c.id),
- ['geh']
- );
- assert.isEmpty(selector('abc'));
- assert.isEmpty(selector('xyz'));
+ const rick = getSafeConversationWithSameTitle(state, {
+ possiblyUnsafeConversation: unique,
+ });
+ assert.strictEqual(rick, undefined);
});
});
diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts
index 7e30187a50..4faca52365 100644
--- a/ts/test-electron/state/ducks/conversations_test.ts
+++ b/ts/test-electron/state/ducks/conversations_test.ts
@@ -34,7 +34,6 @@ import {
updateConversationLookups,
} from '../../../state/ducks/conversations';
import { ReadStatus } from '../../../messages/MessageReadStatus';
-import { ContactSpoofingType } from '../../../util/contactSpoofing';
import type { SingleServePromiseIdString } from '../../../services/singleServePromise';
import { CallMode } from '../../../types/Calling';
import { generateAci, getAciFromPrefix } from '../../../types/ServiceId';
@@ -75,8 +74,7 @@ const {
repairNewestMessage,
repairOldestMessage,
resetAllChatColors,
- reviewGroupMemberNameCollision,
- reviewMessageRequestNameCollision,
+ reviewConversationNameCollision,
setComposeGroupAvatar,
setComposeGroupName,
setComposeSearchTerm,
@@ -523,15 +521,12 @@ describe('both/state/ducks/conversations', () => {
it('closes the contact spoofing review modal if it was open', () => {
const state = {
...getEmptyState(),
- contactSpoofingReview: {
- type: ContactSpoofingType.DirectConversationWithSameTitle as const,
- safeConversationId: 'abc123',
- },
+ hasContactSpoofingReview: true,
};
const action = closeContactSpoofingReview();
const actual = reducer(state, action);
- assert.isUndefined(actual.contactSpoofingReview);
+ assert.isFalse(actual.hasContactSpoofingReview);
});
it("does nothing if the modal wasn't already open", () => {
@@ -1347,31 +1342,13 @@ describe('both/state/ducks/conversations', () => {
});
});
- describe('REVIEW_GROUP_MEMBER_NAME_COLLISION', () => {
- it('starts reviewing a group member name collision', () => {
+ describe('REVIEW_CONVERSATION_NAME_COLLISION', () => {
+ it('starts reviewing a name collision', () => {
const state = getEmptyState();
- const action = reviewGroupMemberNameCollision('abc123');
+ const action = reviewConversationNameCollision();
const actual = reducer(state, action);
- assert.deepEqual(actual.contactSpoofingReview, {
- type: ContactSpoofingType.MultipleGroupMembersWithSameTitle as const,
- groupConversationId: 'abc123',
- });
- });
- });
-
- 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, {
- type: ContactSpoofingType.DirectConversationWithSameTitle as const,
- safeConversationId: 'def',
- });
+ assert.isTrue(actual.hasContactSpoofingReview);
});
});
diff --git a/ts/util/getConversation.ts b/ts/util/getConversation.ts
index 3a5c968518..9fc325fbce 100644
--- a/ts/util/getConversation.ts
+++ b/ts/util/getConversation.ts
@@ -210,6 +210,7 @@ export function getConversation(model: ConversationModel): ConversationType {
phoneNumber: getNumber(attributes),
profileName: getProfileName(attributes),
profileSharing: attributes.profileSharing,
+ profileLastUpdatedAt: attributes.profileLastUpdatedAt,
notSharingPhoneNumber: attributes.notSharingPhoneNumber,
publicParams: attributes.publicParams,
secretParams: attributes.secretParams,