Show Session Switchover Events
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
70cd073a72
commit
dd2493a353
13 changed files with 455 additions and 17 deletions
|
@ -1489,6 +1489,14 @@
|
|||
"messageformat": "Your message history for both chats have been merged here.",
|
||||
"description": "Contents of a dialog shown after clicking 'learn more' button on a conversation merge event."
|
||||
},
|
||||
"icu:PhoneNumberDiscovery--notification--withSharedGroup": {
|
||||
"messageformat": "{phoneNumber} belongs to {conversationTitle}. You're both members of {sharedGroup}.",
|
||||
"description": "Shown when we've discovered a phone number for a contact you've been communicating with."
|
||||
},
|
||||
"icu:PhoneNumberDiscovery--notification--noSharedGroup": {
|
||||
"messageformat": "{phoneNumber} belongs to {conversationTitle}",
|
||||
"description": "Shown when we've discovered a phone number for a contact you've been communicating with, but you have no shared groups."
|
||||
},
|
||||
"icu:quoteThumbnailAlt": {
|
||||
"messageformat": "Thumbnail of image from quoted message",
|
||||
"description": "Used in alt tag of thumbnail images inside of an embedded message quote"
|
||||
|
|
|
@ -471,7 +471,7 @@ export class ConversationController {
|
|||
e164,
|
||||
pni: providedPni,
|
||||
reason,
|
||||
fromPniSignature,
|
||||
fromPniSignature = false,
|
||||
mergeOldAndNew = safeCombineConversations,
|
||||
}: {
|
||||
aci?: AciString;
|
||||
|
@ -526,6 +526,12 @@ export class ConversationController {
|
|||
let unusedMatches: Array<ConvoMatchType> = [];
|
||||
|
||||
let targetConversation: ConversationModel | undefined;
|
||||
let targetOldServiceIds:
|
||||
| {
|
||||
aci?: AciString;
|
||||
pni?: PniString;
|
||||
}
|
||||
| undefined;
|
||||
let matchCount = 0;
|
||||
matches.forEach(item => {
|
||||
const { key, value, match } = item;
|
||||
|
@ -597,6 +603,11 @@ export class ConversationController {
|
|||
);
|
||||
}
|
||||
|
||||
targetOldServiceIds = {
|
||||
aci: targetConversation.getAci(),
|
||||
pni: targetConversation.getPni(),
|
||||
};
|
||||
|
||||
log.info(
|
||||
`${logId}: Applying new value for ${unused.key} to target conversation`
|
||||
);
|
||||
|
@ -686,9 +697,32 @@ export class ConversationController {
|
|||
// `${logId}: Match on ${key} is target conversation - ${match.idForLogging()}`
|
||||
// );
|
||||
targetConversation = match;
|
||||
targetOldServiceIds = {
|
||||
aci: targetConversation.getAci(),
|
||||
pni: targetConversation.getPni(),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// If the change is not coming from PNI Signature, and target conversation
|
||||
// had PNI and has acquired new ACI and/or PNI we should check if it had
|
||||
// a PNI session on the original PNI. If yes - add a PhoneNumberDiscovery notification
|
||||
if (
|
||||
e164 &&
|
||||
pni &&
|
||||
targetConversation &&
|
||||
targetOldServiceIds?.pni &&
|
||||
!fromPniSignature &&
|
||||
(targetOldServiceIds.pni !== pni ||
|
||||
(aci && targetOldServiceIds.aci !== aci))
|
||||
) {
|
||||
mergePromises.push(
|
||||
targetConversation.addPhoneNumberDiscoveryIfNeeded(
|
||||
targetOldServiceIds.pni
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (targetConversation) {
|
||||
return { conversation: targetConversation, mergePromises };
|
||||
}
|
||||
|
|
|
@ -1467,6 +1467,18 @@ export class SignalProtocolStore extends EventEmitter {
|
|||
});
|
||||
}
|
||||
|
||||
async hasSessionWith(serviceId: ServiceIdString): Promise<boolean> {
|
||||
return this.withZone(GLOBAL_ZONE, 'hasSessionWith', async () => {
|
||||
if (!this.sessions) {
|
||||
throw new Error('getOpenDevices: this.sessions not yet cached!');
|
||||
}
|
||||
|
||||
return this._getAllSessions().some(
|
||||
({ fromDB }) => fromDB.serviceId === serviceId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async getOpenDevices(
|
||||
ourServiceId: ServiceIdString,
|
||||
serviceIds: ReadonlyArray<ServiceIdString>,
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import type { PropsType } from './PhoneNumberDiscoveryNotification';
|
||||
import { PhoneNumberDiscoveryNotification } from './PhoneNumberDiscoveryNotification';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/PhoneNumberDiscoveryNotification',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
i18n,
|
||||
conversationTitle: overrideProps.conversationTitle || 'John Fire',
|
||||
phoneNumber: '(555) 333-1111',
|
||||
});
|
||||
|
||||
export function WithoutSharedGroup(): JSX.Element {
|
||||
return <PhoneNumberDiscoveryNotification {...createProps()} />;
|
||||
}
|
||||
|
||||
export function WithSharedGroup(): JSX.Element {
|
||||
return (
|
||||
<PhoneNumberDiscoveryNotification
|
||||
{...createProps()}
|
||||
sharedGroup="Fun Times"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { SystemMessage } from './SystemMessage';
|
||||
import { Emojify } from './Emojify';
|
||||
import { getStringForPhoneNumberDiscovery } from '../../util/getStringForPhoneNumberDiscovery';
|
||||
|
||||
export type PropsDataType = {
|
||||
conversationTitle: string;
|
||||
phoneNumber: string;
|
||||
sharedGroup?: string;
|
||||
};
|
||||
export type PropsType = PropsDataType & {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
// Also known as a Session Switchover Event (SSE)
|
||||
export function PhoneNumberDiscoveryNotification(
|
||||
props: PropsType
|
||||
): JSX.Element {
|
||||
const { conversationTitle, i18n, sharedGroup, phoneNumber } = props;
|
||||
|
||||
const message = getStringForPhoneNumberDiscovery({
|
||||
conversationTitle,
|
||||
i18n,
|
||||
phoneNumber,
|
||||
sharedGroup,
|
||||
});
|
||||
|
||||
return <SystemMessage icon="info" contents={<Emojify text={message} />} />;
|
||||
}
|
|
@ -50,6 +50,8 @@ import type { PropsType as PaymentEventNotificationPropsType } from './PaymentEv
|
|||
import { PaymentEventNotification } from './PaymentEventNotification';
|
||||
import type { PropsDataType as ConversationMergeNotificationPropsType } from './ConversationMergeNotification';
|
||||
import { ConversationMergeNotification } from './ConversationMergeNotification';
|
||||
import type { PropsDataType as PhoneNumberDiscoveryNotificationPropsType } from './PhoneNumberDiscoveryNotification';
|
||||
import { PhoneNumberDiscoveryNotification } from './PhoneNumberDiscoveryNotification';
|
||||
import { SystemMessage } from './SystemMessage';
|
||||
import type { FullJSXType } from '../Intl';
|
||||
import { TimelineMessage } from './TimelineMessage';
|
||||
|
@ -122,6 +124,10 @@ type ConversationMergeNotificationType = {
|
|||
type: 'conversationMerge';
|
||||
data: ConversationMergeNotificationPropsType;
|
||||
};
|
||||
type PhoneNumberDiscoveryNotificationType = {
|
||||
type: 'phoneNumberDiscovery';
|
||||
data: PhoneNumberDiscoveryNotificationPropsType;
|
||||
};
|
||||
type PaymentEventType = {
|
||||
type: 'paymentEvent';
|
||||
data: Omit<PaymentEventNotificationPropsType, 'i18n'>;
|
||||
|
@ -137,6 +143,7 @@ export type TimelineItemType = (
|
|||
| GroupV1MigrationType
|
||||
| GroupV2ChangeType
|
||||
| MessageType
|
||||
| PhoneNumberDiscoveryNotificationType
|
||||
| ProfileChangeNotificationType
|
||||
| ResetSessionNotificationType
|
||||
| SafetyNumberNotificationType
|
||||
|
@ -330,6 +337,14 @@ export const TimelineItem = memo(function TimelineItem({
|
|||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
} else if (item.type === 'phoneNumberDiscovery') {
|
||||
notification = (
|
||||
<PhoneNumberDiscoveryNotification
|
||||
{...reducedProps}
|
||||
{...item.data}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
} else if (item.type === 'resetSessionNotification') {
|
||||
notification = <ResetSessionNotification {...reducedProps} i18n={i18n} />;
|
||||
} else if (item.type === 'profileChange') {
|
||||
|
|
4
ts/model-types.d.ts
vendored
4
ts/model-types.d.ts
vendored
|
@ -181,6 +181,7 @@ export type MessageAttributesType = {
|
|||
| 'incoming'
|
||||
| 'keychange'
|
||||
| 'outgoing'
|
||||
| 'phone-number-discovery'
|
||||
| 'profile-change'
|
||||
| 'story'
|
||||
| 'timer-notification'
|
||||
|
@ -214,6 +215,9 @@ export type MessageAttributesType = {
|
|||
source?: string;
|
||||
sourceServiceId?: ServiceIdString;
|
||||
};
|
||||
phoneNumberDiscovery?: {
|
||||
e164: string;
|
||||
};
|
||||
conversationMerge?: {
|
||||
renderInfo: ConversationRenderInfoType;
|
||||
};
|
||||
|
|
|
@ -3032,6 +3032,59 @@ export class ConversationModel extends window.Backbone
|
|||
this.trigger('newmessage', model);
|
||||
}
|
||||
|
||||
async addPhoneNumberDiscoveryIfNeeded(originalPni: PniString): Promise<void> {
|
||||
const logId = `addPhoneNumberDiscoveryIfNeeded(${this.idForLogging()}, ${originalPni})`;
|
||||
|
||||
const e164 = this.get('e164');
|
||||
|
||||
if (!e164) {
|
||||
log.info(`${logId}: not adding, no e164`);
|
||||
return;
|
||||
}
|
||||
|
||||
const hadSession = await window.textsecure.storage.protocol.hasSessionWith(
|
||||
originalPni
|
||||
);
|
||||
|
||||
if (!hadSession) {
|
||||
log.info(`${logId}: not adding, no PNI session`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`${logId}: adding notification`);
|
||||
const timestamp = Date.now();
|
||||
const message: MessageAttributesType = {
|
||||
id: generateGuid(),
|
||||
conversationId: this.id,
|
||||
type: 'phone-number-discovery',
|
||||
sent_at: timestamp,
|
||||
timestamp,
|
||||
received_at: incrementMessageCounter(),
|
||||
received_at_ms: timestamp,
|
||||
phoneNumberDiscovery: {
|
||||
e164,
|
||||
},
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Unseen,
|
||||
schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY,
|
||||
};
|
||||
|
||||
const id = await window.Signal.Data.saveMessage(message, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
forceSave: true,
|
||||
});
|
||||
const model = window.MessageCache.__DEPRECATED$register(
|
||||
id,
|
||||
new window.Whisper.Message({
|
||||
...message,
|
||||
id,
|
||||
}),
|
||||
'addPhoneNumberDiscoveryIfNeeded'
|
||||
);
|
||||
|
||||
this.trigger('newmessage', model);
|
||||
}
|
||||
|
||||
async addVerifiedChange(
|
||||
verifiedChangeId: string,
|
||||
verified: boolean,
|
||||
|
|
|
@ -98,6 +98,7 @@ import {
|
|||
isUnsupportedMessage,
|
||||
isVerifiedChange,
|
||||
isConversationMerge,
|
||||
isPhoneNumberDiscovery,
|
||||
} from '../state/selectors/message';
|
||||
import type { ReactionAttributesType } from '../messageModifiers/Reactions';
|
||||
import { isInCall } from '../state/selectors/calling';
|
||||
|
@ -139,6 +140,7 @@ import { SeenStatus } from '../MessageSeenStatus';
|
|||
import { isNewReactionReplacingPrevious } from '../reactions/util';
|
||||
import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer';
|
||||
import type { StickerWithHydratedData } from '../types/Stickers';
|
||||
|
||||
import {
|
||||
addToAttachmentDownloadQueue,
|
||||
shouldUseAttachmentDownloadQueue,
|
||||
|
@ -313,6 +315,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
!isGroupV1Migration(attributes) &&
|
||||
!isGroupV2Change(attributes) &&
|
||||
!isKeyChange(attributes) &&
|
||||
!isPhoneNumberDiscovery(attributes) &&
|
||||
!isProfileChange(attributes) &&
|
||||
!isUniversalTimerNotification(attributes) &&
|
||||
!isUnsupportedMessage(attributes) &&
|
||||
|
@ -643,6 +646,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
const isUniversalTimerNotificationValue =
|
||||
isUniversalTimerNotification(attributes);
|
||||
const isConversationMergeValue = isConversationMerge(attributes);
|
||||
const isPhoneNumberDiscoveryValue = isPhoneNumberDiscovery(attributes);
|
||||
|
||||
const isPayment = messageHasPaymentEvent(attributes);
|
||||
|
||||
|
@ -674,7 +678,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
isKeyChangeValue ||
|
||||
isProfileChangeValue ||
|
||||
isUniversalTimerNotificationValue ||
|
||||
isConversationMergeValue;
|
||||
isConversationMergeValue ||
|
||||
isPhoneNumberDiscoveryValue;
|
||||
|
||||
return !hasSomethingToDisplay;
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import type { PropsDataType as GroupV1MigrationPropsType } from '../../component
|
|||
import type { PropsDataType as DeliveryIssuePropsType } from '../../components/conversation/DeliveryIssueNotification';
|
||||
import type { PropsType as PaymentEventNotificationPropsType } from '../../components/conversation/PaymentEventNotification';
|
||||
import type { PropsDataType as ConversationMergePropsType } from '../../components/conversation/ConversationMergeNotification';
|
||||
import type { PropsDataType as PhoneNumberDiscoveryPropsType } from '../../components/conversation/PhoneNumberDiscoveryNotification';
|
||||
import type {
|
||||
PropsData as GroupNotificationProps,
|
||||
ChangeType,
|
||||
|
@ -130,7 +131,11 @@ import { calculateExpirationTimestamp } from '../../util/expirationTimer';
|
|||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import type { AnyPaymentEvent } from '../../types/Payment';
|
||||
import { isPaymentNotificationEvent } from '../../types/Payment';
|
||||
import { getTitleNoDefault, getNumber } from '../../util/getTitle';
|
||||
import {
|
||||
getTitleNoDefault,
|
||||
getNumber,
|
||||
renderNumber,
|
||||
} from '../../util/getTitle';
|
||||
import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp';
|
||||
import type { CallHistorySelectorType } from './callHistory';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
|
@ -951,6 +956,13 @@ export function getPropsForBubble(
|
|||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isPhoneNumberDiscovery(message)) {
|
||||
return {
|
||||
type: 'phoneNumberDiscovery',
|
||||
data: getPropsForPhoneNumberDiscovery(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
messageHasPaymentEvent(message) &&
|
||||
|
@ -1517,6 +1529,34 @@ export function getPropsForConversationMerge(
|
|||
};
|
||||
}
|
||||
|
||||
export function isPhoneNumberDiscovery(
|
||||
message: MessageWithUIFieldsType
|
||||
): boolean {
|
||||
return message.type === 'phone-number-discovery';
|
||||
}
|
||||
export function getPropsForPhoneNumberDiscovery(
|
||||
message: MessageWithUIFieldsType,
|
||||
{ conversationSelector }: GetPropsForBubbleOptions
|
||||
): PhoneNumberDiscoveryPropsType {
|
||||
const { phoneNumberDiscovery } = message;
|
||||
if (!phoneNumberDiscovery) {
|
||||
throw new Error(
|
||||
'getPropsForPhoneNumberDiscovery: message is missing phoneNumberDiscovery!'
|
||||
);
|
||||
}
|
||||
|
||||
const conversation = getConversation(message, conversationSelector);
|
||||
const conversationTitle = conversation.title;
|
||||
const sharedGroup = conversation.sharedGroupNames[0];
|
||||
const { e164 } = phoneNumberDiscovery;
|
||||
|
||||
return {
|
||||
conversationTitle,
|
||||
phoneNumber: renderNumber(e164) ?? e164,
|
||||
sharedGroup,
|
||||
};
|
||||
}
|
||||
|
||||
// Delivery Issue
|
||||
|
||||
export function isDeliveryIssue(message: MessageWithUIFieldsType): boolean {
|
||||
|
|
155
ts/test-mock/pnp/phone_discovery_test.ts
Normal file
155
ts/test-mock/pnp/phone_discovery_test.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { ServiceIdKind, Proto, StorageState } from '@signalapp/mock-server';
|
||||
import type { PrimaryDevice } from '@signalapp/mock-server';
|
||||
import createDebug from 'debug';
|
||||
|
||||
import * as durations from '../../util/durations';
|
||||
import { uuidToBytes } from '../../util/uuidToBytes';
|
||||
import { MY_STORY_ID } from '../../types/Stories';
|
||||
import { toUntaggedPni } from '../../types/ServiceId';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import type { App } from '../bootstrap';
|
||||
|
||||
export const debug = createDebug('mock:test:merge');
|
||||
|
||||
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
|
||||
|
||||
// PhoneNumberDiscovery notifications are also known as a Session Switchover Events (SSE).
|
||||
describe('pnp/phone discovery', function (this: Mocha.Suite) {
|
||||
this.timeout(durations.MINUTE);
|
||||
this.retries(4);
|
||||
|
||||
let bootstrap: Bootstrap;
|
||||
let app: App;
|
||||
let pniContact: PrimaryDevice;
|
||||
let pniIdentityKey: Uint8Array;
|
||||
|
||||
beforeEach(async () => {
|
||||
bootstrap = new Bootstrap({ contactCount: 0 });
|
||||
await bootstrap.init();
|
||||
|
||||
const { server, phone } = bootstrap;
|
||||
|
||||
pniContact = await server.createPrimaryDevice({
|
||||
profileName: 'ACI Contact',
|
||||
});
|
||||
pniIdentityKey = pniContact.getPublicKey(ServiceIdKind.PNI).serialize();
|
||||
|
||||
let state = StorageState.getEmpty();
|
||||
|
||||
state = state.updateAccount({
|
||||
profileKey: phone.profileKey.serialize(),
|
||||
e164: phone.device.number,
|
||||
});
|
||||
|
||||
state = state.addContact(
|
||||
pniContact,
|
||||
{
|
||||
identityState: Proto.ContactRecord.IdentityState.DEFAULT,
|
||||
whitelisted: true,
|
||||
|
||||
identityKey: pniIdentityKey,
|
||||
|
||||
serviceE164: pniContact.device.number,
|
||||
},
|
||||
ServiceIdKind.PNI
|
||||
);
|
||||
|
||||
// Put contact in left pane
|
||||
state = state.pin(pniContact, ServiceIdKind.PNI);
|
||||
|
||||
// Add my story
|
||||
state = state.addRecord({
|
||||
type: IdentifierType.STORY_DISTRIBUTION_LIST,
|
||||
record: {
|
||||
storyDistributionList: {
|
||||
allowsReplies: true,
|
||||
identifier: uuidToBytes(MY_STORY_ID),
|
||||
isBlockList: true,
|
||||
name: MY_STORY_ID,
|
||||
recipientServiceIds: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await phone.setStorageState(state);
|
||||
|
||||
app = await bootstrap.link();
|
||||
});
|
||||
|
||||
afterEach(async function (this: Mocha.Context) {
|
||||
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||
await app.close();
|
||||
await bootstrap.teardown();
|
||||
});
|
||||
|
||||
it('adds phone number discovery when we detect ACI/PNI association via Storage Service', async () => {
|
||||
const { phone } = bootstrap;
|
||||
|
||||
const window = await app.getWindow();
|
||||
const leftPane = window.locator('#LeftPane');
|
||||
|
||||
debug('opening conversation with the PNI');
|
||||
await leftPane.locator(`[data-testid="${pniContact.device.pni}"]`).click();
|
||||
|
||||
debug('Send message to PNI and establish a session');
|
||||
{
|
||||
const compositionInput = await app.waitForEnabledComposer();
|
||||
|
||||
await compositionInput.type('Hello PNI');
|
||||
await compositionInput.press('Enter');
|
||||
}
|
||||
|
||||
debug(
|
||||
'adding both contacts from storage service, adding one combined contact'
|
||||
);
|
||||
{
|
||||
const state = await phone.expectStorageState('consistency check');
|
||||
await phone.setStorageState(
|
||||
state
|
||||
.removeContact(pniContact, ServiceIdKind.PNI)
|
||||
.addContact(pniContact, {
|
||||
identityState: Proto.ContactRecord.IdentityState.DEFAULT,
|
||||
whitelisted: true,
|
||||
identityKey: pniContact.publicKey.serialize(),
|
||||
profileKey: pniContact.profileKey.serialize(),
|
||||
pni: toUntaggedPni(pniContact.device.pni),
|
||||
})
|
||||
);
|
||||
await phone.sendFetchStorage({
|
||||
timestamp: bootstrap.getTimestamp(),
|
||||
});
|
||||
}
|
||||
|
||||
await window.locator('.module-conversation-hero').waitFor();
|
||||
|
||||
debug('Open ACI conversation');
|
||||
await leftPane.locator(`[data-testid="${pniContact.device.aci}"]`).click();
|
||||
|
||||
debug('Wait for PNI conversation to go away');
|
||||
await window
|
||||
.locator(`.module-conversation-hero >> ${pniContact.profileName}`)
|
||||
.waitFor({
|
||||
state: 'hidden',
|
||||
});
|
||||
|
||||
debug('Verify final state');
|
||||
{
|
||||
// Should have PNI message
|
||||
await window.locator('.module-message__text >> "Hello PNI"').waitFor();
|
||||
|
||||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 1, 'message count');
|
||||
|
||||
// One notification - the PhoneNumberDiscovery
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /.* belongs to ACI Contact/);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -13,6 +13,9 @@ import type { App } from '../bootstrap';
|
|||
|
||||
export const debug = createDebug('mock:test:pni-change');
|
||||
|
||||
// Note that all tests also generate an PhoneNumberDiscovery notification, also known as a
|
||||
// Session Switchover Event (SSE). See for reference:
|
||||
// https://github.com/signalapp/Signal-Android-Private/blob/df83c941804512c613a1010b7d8e5ce4f0aec71c/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt#L266-L270
|
||||
describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
||||
this.timeout(durations.MINUTE);
|
||||
this.retries(4);
|
||||
|
@ -68,7 +71,7 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
await bootstrap.teardown();
|
||||
});
|
||||
|
||||
it('shows no identity change if identity key is the same', async () => {
|
||||
it('shows phone number change if identity key is the same, learned via storage service', async () => {
|
||||
const { desktop, phone } = bootstrap;
|
||||
|
||||
const window = await app.getWindow();
|
||||
|
@ -162,13 +165,16 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 1, 'message count');
|
||||
|
||||
// No notifications - PNI changed, but identity key is the same
|
||||
// Only a PhoneNumberDiscovery notification
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
||||
}
|
||||
});
|
||||
|
||||
it('shows identity change if identity key has changed', async () => {
|
||||
it('shows identity and phone number change if identity key has changed', async () => {
|
||||
const { desktop, phone } = bootstrap;
|
||||
|
||||
const window = await app.getWindow();
|
||||
|
@ -262,16 +268,19 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 1, 'message count');
|
||||
|
||||
// One notification - the safety number change
|
||||
// Two notifications - the safety number change and PhoneNumberDiscovery
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
||||
assert.strictEqual(await notifications.count(), 2, 'notification count');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /Safety Number has changed/);
|
||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
||||
|
||||
const second = await notifications.nth(1);
|
||||
assert.match(await second.innerText(), /Safety Number has changed/);
|
||||
}
|
||||
});
|
||||
|
||||
it('shows identity change when sending to contact', async () => {
|
||||
it('shows identity and phone number change on send to contact when e165 has changed owners', async () => {
|
||||
const { desktop, phone } = bootstrap;
|
||||
|
||||
const window = await app.getWindow();
|
||||
|
@ -395,16 +404,19 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 2, 'message count');
|
||||
|
||||
// One notification - the safety number change
|
||||
// Two notifications - the safety number change and PhoneNumberDiscovery
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
||||
assert.strictEqual(await notifications.count(), 2, 'notification count');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /Safety Number has changed/);
|
||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
||||
|
||||
const second = await notifications.nth(1);
|
||||
assert.match(await second.innerText(), /Safety Number has changed/);
|
||||
}
|
||||
});
|
||||
|
||||
it('Sends with no warning when key is the same', async () => {
|
||||
it('Get phone number change warning when e164 leaves contact then goes back to same contact', async () => {
|
||||
const { desktop, phone } = bootstrap;
|
||||
|
||||
const window = await app.getWindow();
|
||||
|
@ -551,9 +563,12 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
|
|||
const messages = window.locator('.module-message__text');
|
||||
assert.strictEqual(await messages.count(), 2, 'message count');
|
||||
|
||||
// No notifications - the key is the same
|
||||
// Only a PhoneNumberDiscovery notification
|
||||
const notifications = window.locator('.SystemMessage');
|
||||
assert.strictEqual(await notifications.count(), 0, 'notification count');
|
||||
assert.strictEqual(await notifications.count(), 1, 'notification count');
|
||||
|
||||
const first = await notifications.first();
|
||||
assert.match(await first.innerText(), /.* belongs to ContactA/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
29
ts/util/getStringForPhoneNumberDiscovery.ts
Normal file
29
ts/util/getStringForPhoneNumberDiscovery.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
||||
export function getStringForPhoneNumberDiscovery({
|
||||
phoneNumber,
|
||||
i18n,
|
||||
conversationTitle,
|
||||
sharedGroup,
|
||||
}: {
|
||||
phoneNumber: string;
|
||||
i18n: LocalizerType;
|
||||
conversationTitle: string;
|
||||
sharedGroup?: string;
|
||||
}): string {
|
||||
if (sharedGroup) {
|
||||
return i18n('icu:PhoneNumberDiscovery--notification--withSharedGroup', {
|
||||
phoneNumber,
|
||||
conversationTitle,
|
||||
sharedGroup,
|
||||
});
|
||||
}
|
||||
|
||||
return i18n('icu:PhoneNumberDiscovery--notification--noSharedGroup', {
|
||||
phoneNumber,
|
||||
conversationTitle,
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue