diff --git a/package.json b/package.json index 493c3b274..1062850f4 100644 --- a/package.json +++ b/package.json @@ -192,7 +192,7 @@ "@babel/preset-typescript": "7.17.12", "@electron/fuses": "1.5.0", "@mixer/parallel-prettier": "2.0.1", - "@signalapp/mock-server": "2.14.0", + "@signalapp/mock-server": "2.15.0", "@storybook/addon-a11y": "6.5.6", "@storybook/addon-actions": "6.5.6", "@storybook/addon-controls": "6.5.6", diff --git a/protos/SignalStorage.proto b/protos/SignalStorage.proto index fe3d15fec..8d2ce79da 100644 --- a/protos/SignalStorage.proto +++ b/protos/SignalStorage.proto @@ -95,6 +95,7 @@ message ContactRecord { optional uint64 unregisteredAtTimestamp = 16; optional string systemGivenName = 17; optional string systemFamilyName = 18; + optional string systemNickname = 19; } message GroupV1Record { diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index 40387f5c4..2cdc63e23 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -327,6 +327,7 @@ export type ConversationAttributesType = { name?: string; systemGivenName?: string; systemFamilyName?: string; + systemNickname?: string; needsStorageServiceSync?: boolean; needsVerification?: boolean; profileSharing?: boolean; diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 104a69bd9..338d88786 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -352,7 +352,8 @@ export class ConversationModel extends window.Backbone this.on('newmessage', this.onNewMessage); this.on('change:profileKey', this.onChangeProfileKey); this.on( - 'change:name change:profileName change:profileFamilyName change:e164', + 'change:name change:profileName change:profileFamilyName change:e164 ' + + 'change:systemGivenName change:systemFamilyName change:systemNickname', () => this.maybeClearUsername() ); @@ -1910,6 +1911,7 @@ export class ConversationModel extends window.Backbone name: this.get('name'), systemGivenName: this.get('systemGivenName'), systemFamilyName: this.get('systemFamilyName'), + systemNickname: this.get('systemNickname'), phoneNumber: this.getNumber(), profileName: this.getProfileName(), profileSharing: this.get('profileSharing'), diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 17206cb3a..426f8ef85 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -198,6 +198,10 @@ export async function toContactRecord( if (systemFamilyName) { contactRecord.systemFamilyName = systemFamilyName; } + const systemNickname = conversation.get('systemNickname'); + if (systemNickname) { + contactRecord.systemNickname = systemNickname; + } contactRecord.blocked = conversation.isBlocked(); contactRecord.whitelisted = Boolean(conversation.get('profileSharing')); contactRecord.archived = Boolean(conversation.get('isArchived')); @@ -1033,6 +1037,7 @@ export async function mergeContactRecord( conversation.set({ systemGivenName: dropNull(contactRecord.systemGivenName), systemFamilyName: dropNull(contactRecord.systemFamilyName), + systemNickname: dropNull(contactRecord.systemNickname), }); // https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt#L921-L936 diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index c71ada211..64c909ac8 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -193,6 +193,7 @@ export type ConversationType = ReadonlyDeep< name?: string; systemGivenName?: string; systemFamilyName?: string; + systemNickname?: string; familyName?: string; firstName?: string; profileName?: string; diff --git a/ts/test-mock/pnp/username_test.ts b/ts/test-mock/pnp/username_test.ts index c25aca309..658a98f4f 100644 --- a/ts/test-mock/pnp/username_test.ts +++ b/ts/test-mock/pnp/username_test.ts @@ -80,60 +80,75 @@ describe('pnp/username', function needsName() { await bootstrap.teardown(); }); - it('drops username when contact name becomes known', async () => { - const { phone } = bootstrap; + for (const type of ['profile', 'system']) { + // eslint-disable-next-line no-loop-func + it(`drops username when contact's ${type} name becomes known`, async () => { + const { phone } = bootstrap; - const window = await app.getWindow(); - const leftPane = window.locator('.left-pane-wrapper'); + const window = await app.getWindow(); + const leftPane = window.locator('.left-pane-wrapper'); - debug('find username in the left pane'); - await leftPane - .locator( - `[data-testid="${usernameContact.device.uuid}"] >> "${USERNAME}"` - ) - .waitFor(); + debug('find username in the left pane'); + await leftPane + .locator( + `[data-testid="${usernameContact.device.uuid}"] >> "${USERNAME}"` + ) + .waitFor(); - debug('adding profile key for username contact'); - let state = await phone.expectStorageState('consistency check'); - state = state.updateContact(usernameContact, { - profileKey: usernameContact.profileKey.serialize(), - }); - await phone.setStorageState(state); - await phone.sendFetchStorage({ - timestamp: bootstrap.getTimestamp(), - }); + let state = await phone.expectStorageState('consistency check'); - debug('find profile name in the left pane'); - await leftPane - .locator( - `[data-testid="${usernameContact.device.uuid}"] >> ` + - `"${usernameContact.profileName}"` - ) - .waitFor(); - - debug('verify that storage service state is updated'); - { - const newState = await phone.waitForStorageState({ - after: state, + if (type === 'profile') { + debug('adding profile key for username contact'); + state = state.updateContact(usernameContact, { + profileKey: usernameContact.profileKey.serialize(), + }); + } else { + debug('adding nickname for username contact'); + state = state.updateContact(usernameContact, { + systemNickname: usernameContact.profileName, + }); + } + await phone.setStorageState(state); + await phone.sendFetchStorage({ + timestamp: bootstrap.getTimestamp(), }); - const { added, removed } = newState.diff(state); - assert.strictEqual(added.length, 1, 'only one record must be added'); - assert.strictEqual(removed.length, 1, 'only one record must be removed'); + debug('find profile name in the left pane'); + await leftPane + .locator( + `[data-testid="${usernameContact.device.uuid}"] >> ` + + `"${usernameContact.profileName}"` + ) + .waitFor(); - assert.strictEqual( - added[0].contact?.serviceUuid, - usernameContact.device.uuid - ); - assert.strictEqual(added[0].contact?.username, ''); + debug('verify that storage service state is updated'); + { + const newState = await phone.waitForStorageState({ + after: state, + }); - assert.strictEqual( - removed[0].contact?.serviceUuid, - usernameContact.device.uuid - ); - assert.strictEqual(removed[0].contact?.username, USERNAME); - } - }); + const { added, removed } = newState.diff(state); + assert.strictEqual(added.length, 1, 'only one record must be added'); + assert.strictEqual( + removed.length, + 1, + 'only one record must be removed' + ); + + assert.strictEqual( + added[0].contact?.serviceUuid, + usernameContact.device.uuid + ); + assert.strictEqual(added[0].contact?.username, ''); + + assert.strictEqual( + removed[0].contact?.serviceUuid, + usernameContact.device.uuid + ); + assert.strictEqual(removed[0].contact?.username, USERNAME); + } + }); + } it('reserves/confirms/deletes username', async () => { const { phone, server } = bootstrap; diff --git a/ts/util/getTitle.ts b/ts/util/getTitle.ts index 2b7a68518..03a566641 100644 --- a/ts/util/getTitle.ts +++ b/ts/util/getTitle.ts @@ -49,7 +49,16 @@ export function getTitleNoDefault( export function canHaveUsername( attributes: Pick< ConversationAttributesType, - 'id' | 'type' | 'name' | 'profileName' | 'profileFamilyName' | 'e164' + | 'id' + | 'type' + | 'name' + | 'profileName' + | 'profileFamilyName' + | 'e164' + | 'systemGivenName' + | 'systemFamilyName' + | 'systemNickname' + | 'type' >, ourConversationId: string | undefined ): boolean { @@ -84,13 +93,13 @@ export function getProfileName( export function getSystemName( attributes: Pick< ConversationAttributesType, - 'systemGivenName' | 'systemFamilyName' | 'type' + 'systemGivenName' | 'systemFamilyName' | 'systemNickname' | 'type' > ): string | undefined { if (isDirectConversation(attributes)) { - return combineNames( - attributes.systemGivenName, - attributes.systemFamilyName + return ( + attributes.systemNickname || + combineNames(attributes.systemGivenName, attributes.systemFamilyName) ); } diff --git a/yarn.lock b/yarn.lock index b79617212..3955e4c7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2170,10 +2170,10 @@ node-gyp-build "^4.2.3" uuid "^8.3.0" -"@signalapp/mock-server@2.14.0": - version "2.14.0" - resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.14.0.tgz#6309d944cf46e58f6141df45075de882d964ae0a" - integrity sha512-NSLnfjho4HCyrz4Y6cyoIK0f+iuhOrxEFmaabdVUepOaSHPZi1MTyYYo0d6NzD/PGREAQFJuzqNaE+7zhSiPEQ== +"@signalapp/mock-server@2.15.0": + version "2.15.0" + resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.15.0.tgz#de86ddc4c3f7cbe1e91941832c4b317946e90364" + integrity sha512-bxu4hpnEAAvDT7Yg2LZQNIL/9ciNrGG0hPJlj+dT2iwsHo2AAP8Ej4sLfAiy0O2kYbf2bKcvfTE9C+XwkdAW+w== dependencies: "@signalapp/libsignal-client" "^0.22.0" debug "^4.3.2"