Don't show name collisions for system contacts
This commit is contained in:
parent
84be8288e9
commit
6c6eed0b1e
12 changed files with 121 additions and 17 deletions
|
@ -11,6 +11,7 @@ import { InContactsIcon } from './InContactsIcon';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
import { sortByTitle } from '../util/sortByTitle';
|
import { sortByTitle } from '../util/sortByTitle';
|
||||||
import { ConversationType } from '../state/ducks/conversations';
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||||
|
|
||||||
type ParticipantType = ConversationType & {
|
type ParticipantType = ConversationType & {
|
||||||
hasRemoteAudio?: boolean;
|
hasRemoteAudio?: boolean;
|
||||||
|
@ -118,7 +119,7 @@ export const CallingParticipantsList = React.memo(
|
||||||
module="module-calling-participants-list__name"
|
module="module-calling-participants-list__name"
|
||||||
title={participant.title}
|
title={participant.title}
|
||||||
/>
|
/>
|
||||||
{participant.name ? (
|
{isInSystemContacts(participant) ? (
|
||||||
<span>
|
<span>
|
||||||
{' '}
|
{' '}
|
||||||
<InContactsIcon
|
<InContactsIcon
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { InContactsIcon } from './InContactsIcon';
|
||||||
|
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
import { ConversationType } from '../state/ducks/conversations';
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -69,10 +70,18 @@ export class ContactListItem extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const { about, i18n, isAdmin, isMe, name, onClick, title } = this.props;
|
const {
|
||||||
|
about,
|
||||||
|
i18n,
|
||||||
|
isAdmin,
|
||||||
|
isMe,
|
||||||
|
name,
|
||||||
|
onClick,
|
||||||
|
title,
|
||||||
|
type,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const displayName = isMe ? i18n('you') : title;
|
const displayName = isMe ? i18n('you') : title;
|
||||||
const shouldShowIcon = Boolean(name);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -88,7 +97,7 @@ export class ContactListItem extends React.Component<Props> {
|
||||||
<div className="module-contact-list-item__left">
|
<div className="module-contact-list-item__left">
|
||||||
<div className="module-contact-list-item__text__name">
|
<div className="module-contact-list-item__text__name">
|
||||||
<Emojify text={displayName} />
|
<Emojify text={displayName} />
|
||||||
{shouldShowIcon ? (
|
{isInSystemContacts({ name, type }) ? (
|
||||||
<span>
|
<span>
|
||||||
{' '}
|
{' '}
|
||||||
<InContactsIcon i18n={i18n} />
|
<InContactsIcon i18n={i18n} />
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { Modal } from './Modal';
|
||||||
|
|
||||||
import { ConversationType } from '../state/ducks/conversations';
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||||
|
|
||||||
export type SafetyNumberProps = {
|
export type SafetyNumberProps = {
|
||||||
contactID: string;
|
contactID: string;
|
||||||
|
@ -103,7 +104,7 @@ export const SafetyNumberChangeDialog = ({
|
||||||
<div className="module-SafetyNumberChangeDialog__contact--wrapper">
|
<div className="module-SafetyNumberChangeDialog__contact--wrapper">
|
||||||
<div className="module-SafetyNumberChangeDialog__contact--name">
|
<div className="module-SafetyNumberChangeDialog__contact--name">
|
||||||
{contact.title}
|
{contact.title}
|
||||||
{contact.name ? (
|
{isInSystemContacts(contact) ? (
|
||||||
<span>
|
<span>
|
||||||
{' '}
|
{' '}
|
||||||
<InContactsIcon i18n={i18n} />
|
<InContactsIcon i18n={i18n} />
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { Intl } from '../Intl';
|
||||||
import { Emojify } from './Emojify';
|
import { Emojify } from './Emojify';
|
||||||
import { assert } from '../../util/assert';
|
import { assert } from '../../util/assert';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -271,7 +272,7 @@ export const ContactSpoofingReviewDialog: FunctionComponent<PropsType> = props =
|
||||||
{i18n('MessageRequests--unblock')}
|
{i18n('MessageRequests--unblock')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
} else if (!conversationInfo.conversation.name) {
|
} else if (!isInSystemContacts(conversationInfo.conversation)) {
|
||||||
button = (
|
button = (
|
||||||
<Button
|
<Button
|
||||||
variant={ButtonVariant.SecondaryDestructive}
|
variant={ButtonVariant.SecondaryDestructive}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { MuteOption, getMuteOptions } from '../../util/getMuteOptions';
|
||||||
import * as expirationTimer from '../../util/expirationTimer';
|
import * as expirationTimer from '../../util/expirationTimer';
|
||||||
import { isMuted } from '../../util/isMuted';
|
import { isMuted } from '../../util/isMuted';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||||
|
|
||||||
export enum OutgoingCallButtonStyle {
|
export enum OutgoingCallButtonStyle {
|
||||||
None,
|
None,
|
||||||
|
@ -155,12 +156,10 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldShowIcon = Boolean(name && type === 'direct');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-ConversationHeader__header__info__title">
|
<div className="module-ConversationHeader__header__info__title">
|
||||||
<Emojify text={title} />
|
<Emojify text={title} />
|
||||||
{shouldShowIcon ? (
|
{isInSystemContacts({ name, type }) ? (
|
||||||
<InContactsIcon
|
<InContactsIcon
|
||||||
className="module-ConversationHeader__header__info__title__in-contacts-icon"
|
className="module-ConversationHeader__header__info__title__in-contacts-icon"
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import memoizee from 'memoizee';
|
import memoizee from 'memoizee';
|
||||||
import { fromPairs, isNumber, isString } from 'lodash';
|
import { fromPairs, isNumber } from 'lodash';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
@ -38,6 +38,7 @@ import {
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
} from './user';
|
} from './user';
|
||||||
import { getPinnedConversationIds } from './items';
|
import { getPinnedConversationIds } from './items';
|
||||||
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||||
|
|
||||||
let placeholderContact: ConversationType;
|
let placeholderContact: ConversationType;
|
||||||
export const getPlaceholderContact = (): ConversationType => {
|
export const getPlaceholderContact = (): ConversationType => {
|
||||||
|
@ -366,7 +367,7 @@ function isTrusted(conversation: ConversationType): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Boolean(
|
return Boolean(
|
||||||
isString(conversation.name) ||
|
isInSystemContacts(conversation) ||
|
||||||
conversation.profileSharing ||
|
conversation.profileSharing ||
|
||||||
conversation.isMe
|
conversation.isMe
|
||||||
);
|
);
|
||||||
|
|
|
@ -46,6 +46,7 @@ describe('group member name collision utilities', () => {
|
||||||
title: 'Bob',
|
title: 'Bob',
|
||||||
});
|
});
|
||||||
const bob2 = getDefaultConversation({
|
const bob2 = getDefaultConversation({
|
||||||
|
name: 'Bob In Your Contacts',
|
||||||
profileName: 'Bob',
|
profileName: 'Bob',
|
||||||
title: 'Bob',
|
title: 'Bob',
|
||||||
});
|
});
|
||||||
|
@ -53,19 +54,44 @@ describe('group member name collision utilities', () => {
|
||||||
profileName: 'Bob',
|
profileName: 'Bob',
|
||||||
title: 'Bob',
|
title: 'Bob',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ignored, because Bob is not in your contacts (lacks `name`), has no profile name,
|
||||||
|
// and has no E164.
|
||||||
const ignoredBob = getDefaultConversation({
|
const ignoredBob = getDefaultConversation({
|
||||||
e164: undefined,
|
e164: undefined,
|
||||||
title: 'Bob',
|
title: 'Bob',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ignored, because there's only one Charlie.
|
||||||
const charlie = getDefaultConversation({
|
const charlie = getDefaultConversation({
|
||||||
profileName: 'Charlie',
|
profileName: 'Charlie',
|
||||||
title: 'Charlie',
|
title: 'Charlie',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ignored, because all Donnas are in your contacts (they have a `name`).
|
||||||
|
const donna1 = getDefaultConversation({
|
||||||
|
name: 'Donna One',
|
||||||
|
profileName: 'Donna',
|
||||||
|
title: 'Donna',
|
||||||
|
});
|
||||||
|
const donna2 = getDefaultConversation({
|
||||||
|
name: 'Donna Two',
|
||||||
|
profileName: 'Donna',
|
||||||
|
title: 'Donna',
|
||||||
|
});
|
||||||
|
const donna3 = getDefaultConversation({
|
||||||
|
name: 'Donna Three',
|
||||||
|
profileName: 'Donna',
|
||||||
|
title: 'Donna',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ignored, because you're not included.
|
||||||
const me = getDefaultConversation({
|
const me = getDefaultConversation({
|
||||||
isMe: true,
|
isMe: true,
|
||||||
profileName: 'Alice',
|
profileName: 'Alice',
|
||||||
title: 'Alice',
|
title: 'Alice',
|
||||||
});
|
});
|
||||||
|
|
||||||
const memberships = [
|
const memberships = [
|
||||||
alice1,
|
alice1,
|
||||||
alice2,
|
alice2,
|
||||||
|
@ -74,6 +100,9 @@ describe('group member name collision utilities', () => {
|
||||||
bob3,
|
bob3,
|
||||||
ignoredBob,
|
ignoredBob,
|
||||||
charlie,
|
charlie,
|
||||||
|
donna1,
|
||||||
|
donna2,
|
||||||
|
donna3,
|
||||||
me,
|
me,
|
||||||
].map(member => ({ member }));
|
].map(member => ({ member }));
|
||||||
|
|
||||||
|
|
46
ts/test-both/util/isInSystemContacts_test.ts
Normal file
46
ts/test-both/util/isInSystemContacts_test.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||||
|
|
||||||
|
describe('isInSystemContacts', () => {
|
||||||
|
it('returns true for direct conversations that have a `name` property', () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isInSystemContacts({
|
||||||
|
type: 'direct',
|
||||||
|
name: 'Jane Doe',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
isInSystemContacts({
|
||||||
|
type: 'private',
|
||||||
|
name: 'Jane Doe',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for direct conversations that have an empty string `name`', () => {
|
||||||
|
assert.isTrue(
|
||||||
|
isInSystemContacts({
|
||||||
|
type: 'direct',
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for direct conversations that lack a `name` property', () => {
|
||||||
|
assert.isFalse(isInSystemContacts({ type: 'direct' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for group conversations', () => {
|
||||||
|
assert.isFalse(isInSystemContacts({ type: 'group' }));
|
||||||
|
assert.isFalse(
|
||||||
|
isInSystemContacts({
|
||||||
|
type: 'group',
|
||||||
|
name: 'Tahoe Trip',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,6 +6,7 @@ import { SendMetadataType, SendOptionsType } from '../textsecure/SendMessage';
|
||||||
import { arrayBufferToBase64, getRandomBytes } from '../Crypto';
|
import { arrayBufferToBase64, getRandomBytes } from '../Crypto';
|
||||||
import { getConversationMembers } from './getConversationMembers';
|
import { getConversationMembers } from './getConversationMembers';
|
||||||
import { isDirectConversation, isMe } from './whatTypeOfConversation';
|
import { isDirectConversation, isMe } from './whatTypeOfConversation';
|
||||||
|
import { isInSystemContacts } from './isInSystemContacts';
|
||||||
import { missingCaseError } from './missingCaseError';
|
import { missingCaseError } from './missingCaseError';
|
||||||
import { senderCertificateService } from '../services/senderCertificate';
|
import { senderCertificateService } from '../services/senderCertificate';
|
||||||
import {
|
import {
|
||||||
|
@ -117,13 +118,11 @@ function getSenderCertificateForDirectConversation(
|
||||||
case PhoneNumberSharingMode.Everybody:
|
case PhoneNumberSharingMode.Everybody:
|
||||||
certificateMode = SenderCertificateMode.WithE164;
|
certificateMode = SenderCertificateMode.WithE164;
|
||||||
break;
|
break;
|
||||||
case PhoneNumberSharingMode.ContactsOnly: {
|
case PhoneNumberSharingMode.ContactsOnly:
|
||||||
const isInSystemContacts = Boolean(conversationAttrs.name);
|
certificateMode = isInSystemContacts(conversationAttrs)
|
||||||
certificateMode = isInSystemContacts
|
|
||||||
? SenderCertificateMode.WithE164
|
? SenderCertificateMode.WithE164
|
||||||
: SenderCertificateMode.WithoutE164;
|
: SenderCertificateMode.WithoutE164;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case PhoneNumberSharingMode.Nobody:
|
case PhoneNumberSharingMode.Nobody:
|
||||||
certificateMode = SenderCertificateMode.WithoutE164;
|
certificateMode = SenderCertificateMode.WithoutE164;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { groupBy, map, filter } from './iterables';
|
||||||
import { getOwn } from './getOwn';
|
import { getOwn } from './getOwn';
|
||||||
import { ConversationType } from '../state/ducks/conversations';
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
import { isConversationNameKnown } from './isConversationNameKnown';
|
import { isConversationNameKnown } from './isConversationNameKnown';
|
||||||
|
import { isInSystemContacts } from './isInSystemContacts';
|
||||||
|
|
||||||
export type GroupNameCollisionsWithIdsByTitle = Record<string, Array<string>>;
|
export type GroupNameCollisionsWithIdsByTitle = Record<string, Array<string>>;
|
||||||
export type GroupNameCollisionsWithConversationsByTitle = Record<
|
export type GroupNameCollisionsWithConversationsByTitle = Record<
|
||||||
|
@ -37,7 +38,8 @@ export function getCollisionsFromMemberships(
|
||||||
// [0]: https://www.typescriptlang.org/play?#code/C4TwDgpgBAYg9nKBeKAFAhgJ2AS3QGwB4AlCAYzkwBNCBnYTHAOwHMAaKJgVwFsAjCJgB8QgNwAoCk3pQAZgC5YCZFADeUABY5FAVigBfCeNCQoAISwrSFanQbN2nXgOESpMvoouYVs0UA
|
// [0]: https://www.typescriptlang.org/play?#code/C4TwDgpgBAYg9nKBeKAFAhgJ2AS3QGwB4AlCAYzkwBNCBnYTHAOwHMAaKJgVwFsAjCJgB8QgNwAoCk3pQAZgC5YCZFADeUABY5FAVigBfCeNCQoAISwrSFanQbN2nXgOESpMvoouYVs0UA
|
||||||
return (pickBy(
|
return (pickBy(
|
||||||
groupedByTitle,
|
groupedByTitle,
|
||||||
group => group.length >= 2
|
group =>
|
||||||
|
group.length >= 2 && !group.every(person => isInSystemContacts(person))
|
||||||
) as unknown) as GroupNameCollisionsWithConversationsByTitle;
|
) as unknown) as GroupNameCollisionsWithConversationsByTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import { ConversationAttributesType } from '../model-types.d';
|
import { ConversationAttributesType } from '../model-types.d';
|
||||||
import { isDirectConversation, isMe } from './whatTypeOfConversation';
|
import { isDirectConversation, isMe } from './whatTypeOfConversation';
|
||||||
|
import { isInSystemContacts } from './isInSystemContacts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if this conversation should be considered "accepted" in terms
|
* Determine if this conversation should be considered "accepted" in terms
|
||||||
|
@ -63,7 +64,10 @@ function isFromOrAddedByTrustedContact(
|
||||||
conversationAttrs: ConversationAttributesType
|
conversationAttrs: ConversationAttributesType
|
||||||
): boolean {
|
): boolean {
|
||||||
if (isDirectConversation(conversationAttrs)) {
|
if (isDirectConversation(conversationAttrs)) {
|
||||||
return Boolean(conversationAttrs.name || conversationAttrs.profileSharing);
|
return (
|
||||||
|
isInSystemContacts(conversationAttrs) ||
|
||||||
|
Boolean(conversationAttrs.profileSharing)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { addedBy } = conversationAttrs;
|
const { addedBy } = conversationAttrs;
|
||||||
|
|
12
ts/util/isInSystemContacts.ts
Normal file
12
ts/util/isInSystemContacts.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export const isInSystemContacts = ({
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
}: Readonly<{
|
||||||
|
type?: string;
|
||||||
|
name?: string;
|
||||||
|
}>): boolean =>
|
||||||
|
// `direct` for redux, `private` for models and the database
|
||||||
|
(type === 'direct' || type === 'private') && typeof name === 'string';
|
Loading…
Reference in a new issue