Change Phone Number notifications

This commit is contained in:
Fedor Indutny 2021-08-05 16:34:49 -07:00 committed by GitHub
parent 4b82ac387b
commit a001882d58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 277 additions and 39 deletions

View File

@ -1168,6 +1168,16 @@
}
}
},
"ChangeNumber--notification": {
"message": "$sender$ changed their number to a new number",
"description": "Shown in timeline when a member of a conversation changes their phone number",
"placeholders": {
"sender": {
"content": "$1",
"example": "Sam"
}
}
},
"quoteThumbnailAlt": {
"message": "Thumbnail of image from quoted message",
"description": "Used in alt tag of thumbnail images inside of an embedded message quote"

View File

@ -135,4 +135,5 @@ message AccountRecord {
repeated PinnedConversation pinnedConversations = 14;
optional uint32 universalExpireTimer = 17;
optional bool primarySendsSms = 18;
optional string e164 = 19;
}

View File

@ -2425,6 +2425,45 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
}
}
// Module: Change Number Notification
.module-change-number-notification {
@include font-body-2;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-05;
}
&__icon {
height: 16px;
width: 16px;
display: inline-block;
margin-right: 8px;
@include light-theme {
@include color-svg(
'../images/icons/v2/phone-right-outline-24.svg',
$color-gray-75
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/phone-right-solid-24.svg',
$color-gray-15
);
}
}
}
// Module: Error Boundary
.module-error-boundary-notification {
text-align: center;
cursor: pointer;

View File

@ -60,6 +60,7 @@ import {
ReadSyncEvent,
ContactEvent,
GroupEvent,
EnvelopeEvent,
} from './textsecure/messageReceiverEvents';
import type { WebAPIType } from './textsecure/WebAPI';
import * as universalExpireTimer from './util/universalExpireTimer';
@ -174,6 +175,10 @@ export async function startApp(): Promise<void> {
};
}
messageReceiver.addEventListener(
'envelope',
queuedEventListener(onEnvelopeReceived, false)
);
messageReceiver.addEventListener(
'message',
queuedEventListener(onMessageReceived, false)
@ -503,17 +508,7 @@ export async function startApp(): Promise<void> {
accountManager = new window.textsecure.AccountManager(server);
accountManager.addEventListener('registration', () => {
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const user = {
ourConversationId: window.ConversationController.getOurConversationId(),
ourDeviceId,
ourNumber,
ourUuid,
regionCode: window.storage.get('regionCode'),
};
window.Whisper.events.trigger('userChanged', user);
window.Whisper.events.trigger('userChanged');
window.Signal.Util.Registration.markDone();
window.log.info('dispatching registration event');
@ -1210,7 +1205,6 @@ export async function startApp(): Promise<void> {
conversationRemoved,
removeAllConversations,
} = window.reduxActions.conversations;
const { userChanged } = window.reduxActions.user;
convoCollection.on('remove', conversation => {
const { id } = conversation || {};
@ -1254,7 +1248,19 @@ export async function startApp(): Promise<void> {
});
convoCollection.on('reset', removeAllConversations);
window.Whisper.events.on('userChanged', userChanged);
window.Whisper.events.on('userChanged', () => {
const newDeviceId = window.textsecure.storage.user.getDeviceId();
const newNumber = window.textsecure.storage.user.getNumber();
const newUuid = window.textsecure.storage.user.getUuid();
window.reduxActions.user.userChanged({
ourConversationId: window.ConversationController.getOurConversationId(),
ourDeviceId: newDeviceId,
ourNumber: newNumber,
ourUuid: newUuid,
regionCode: window.storage.get('regionCode'),
});
});
let shortcutGuideView: WhatIsThis | null = null;
@ -3000,6 +3006,17 @@ export async function startApp(): Promise<void> {
maxSize: Infinity,
});
function onEnvelopeReceived({ envelope }: EnvelopeEvent) {
const ourUuid = window.textsecure.storage.user.getUuid();
if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) {
window.ConversationController.ensureContactIds({
e164: envelope.source,
uuid: envelope.sourceUuid,
highTrust: true,
});
}
}
// Note: We do very little in this function, since everything in handleDataMessage is
// inside a conversation-specific queue(). Any code here might run before an earlier
// message is processed in handleDataMessage().

View File

@ -0,0 +1,43 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { ConversationType } from '../../state/ducks/conversations';
import { LocalizerType } from '../../types/Util';
import { Intl } from '../Intl';
import { Timestamp } from './Timestamp';
import { Emojify } from './Emojify';
export type PropsData = {
sender: ConversationType;
timestamp: number;
};
export type PropsHousekeeping = {
i18n: LocalizerType;
};
export type Props = PropsData & PropsHousekeeping;
const CSS_MODULE = 'module-change-number-notification';
export const ChangeNumberNotification: React.FC<Props> = props => {
const { i18n, sender, timestamp } = props;
return (
<div className={CSS_MODULE}>
<span className={`${CSS_MODULE}__icon`} />
<Intl
id="ChangeNumber--notification"
components={{
sender: <Emojify text={sender.firstName || sender.title} />,
}}
i18n={i18n}
/>
&nbsp;·&nbsp;
<Timestamp i18n={i18n} timestamp={timestamp} />
</div>
);
};

View File

@ -129,6 +129,13 @@ storiesOf('Components/Conversation/TimelineItem', module)
type: 'universalTimerNotification',
data: null,
},
{
type: 'changeNumberNotification',
data: {
sender: getDefaultConversation(),
timestamp: Date.now(),
},
},
{
type: 'callHistory',
data: {

View File

@ -24,6 +24,10 @@ import {
PropsActionsType as DeliveryIssueActionProps,
PropsDataType as DeliveryIssueProps,
} from './DeliveryIssueNotification';
import {
ChangeNumberNotification,
PropsData as ChangeNumberNotificationProps,
} from './ChangeNumberNotification';
import { CallingNotificationType } from '../../util/callingNotification';
import { InlineNotificationWrapper } from './InlineNotificationWrapper';
import {
@ -95,6 +99,10 @@ type UniversalTimerNotificationType = {
type: 'universalTimerNotification';
data: null;
};
type ChangeNumberNotificationType = {
type: 'changeNumberNotification';
data: ChangeNumberNotificationProps;
};
type SafetyNumberNotificationType = {
type: 'safetyNumberNotification';
data: SafetyNumberNotificationProps;
@ -138,6 +146,7 @@ export type TimelineItemType =
| SafetyNumberNotificationType
| TimerNotificationType
| UniversalTimerNotificationType
| ChangeNumberNotificationType
| UnsupportedMessageType
| VerificationNotificationType;
@ -244,6 +253,10 @@ export class TimelineItem extends React.PureComponent<PropsType> {
);
} else if (item.type === 'universalTimerNotification') {
notification = renderUniversalTimerNotification();
} else if (item.type === 'changeNumberNotification') {
notification = (
<ChangeNumberNotification {...this.props} {...item.data} i18n={i18n} />
);
} else if (item.type === 'safetyNumberNotification') {
notification = (
<SafetyNumberNotification {...this.props} {...item.data} i18n={i18n} />

1
ts/model-types.d.ts vendored
View File

@ -141,6 +141,7 @@ export type MessageAttributesType = {
| 'profile-change'
| 'timer-notification'
| 'universal-timer-notification'
| 'change-number-notification'
| 'verified-change';
body?: string;
attachments?: Array<AttachmentType>;

View File

@ -152,8 +152,6 @@ export class ConversationModel extends window.Backbone
jobQueue?: typeof window.PQueueType;
ourNumber?: string;
ourUuid?: string;
storeName?: string | null;
@ -234,7 +232,6 @@ export class ConversationModel extends window.Backbone
this.storeName = 'conversations';
this.ourNumber = window.textsecure.storage.user.getNumber();
this.ourUuid = window.textsecure.storage.user.getUuid();
this.verifiedEnum = window.textsecure.storage.protocol.VerifiedStatus;
@ -1497,6 +1494,11 @@ export class ConversationModel extends window.Backbone
const oldValue = this.get('e164');
if (e164 && e164 !== oldValue) {
this.set('e164', e164);
if (oldValue) {
this.addChangeNumberNotification();
}
window.Signal.Data.updateConversation(this.attributes);
this.trigger('idUpdated', this, 'e164', oldValue);
}
@ -2688,7 +2690,7 @@ export class ConversationModel extends window.Backbone
sent_at: now,
received_at: window.Signal.Util.incrementMessageCounter(),
received_at_ms: now,
unread: 0,
unread: false,
changedId: conversationId || this.id,
profileChange,
// TODO: DESKTOP-722
@ -2716,23 +2718,30 @@ export class ConversationModel extends window.Backbone
}
}
async addUniversalTimerNotification(): Promise<string> {
async addNotification(
type: MessageAttributesType['type'],
extra: Partial<MessageAttributesType> = {}
): Promise<string> {
const now = Date.now();
const message = ({
const message: Partial<MessageAttributesType> = {
...extra,
conversationId: this.id,
type: 'universal-timer-notification',
type,
sent_at: now,
received_at: window.Signal.Util.incrementMessageCounter(),
received_at_ms: now,
unread: 0,
// TODO: DESKTOP-722
} as unknown) as typeof window.Whisper.MessageAttributesType;
unread: false,
};
const id = await window.Signal.Data.saveMessage(message);
const id = await window.Signal.Data.saveMessage(
// TODO: DESKTOP-722
message as MessageAttributesType
);
const model = window.MessageController.register(
id,
new window.Whisper.Message({
...message,
...(message as MessageAttributesType),
id,
})
);
@ -2764,7 +2773,9 @@ export class ConversationModel extends window.Backbone
return;
}
const notificationId = await this.addUniversalTimerNotification();
const notificationId = await this.addNotification(
'universal-timer-notification'
);
this.set('pendingUniversalTimer', notificationId);
}
@ -2797,6 +2808,27 @@ export class ConversationModel extends window.Backbone
this.set('pendingUniversalTimer', undefined);
}
async addChangeNumberNotification(): Promise<void> {
window.log.info(
`Conversation ${this.idForLogging()}: adding change number notification`
);
const convos = [
this,
...(await window.ConversationController.getAllGroupsInvolvingId(this.id)),
];
const sourceUuid = this.get('uuid');
await Promise.all(
convos.map(convo => {
return convo.addNotification('change-number-notification', {
sourceUuid,
});
})
);
}
async onReadMessage(
message: MessageModel,
readAt?: number

View File

@ -178,8 +178,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
INITIAL_PROTOCOL_VERSION?: number;
OUR_NUMBER?: string;
OUR_UUID?: string;
isSelected?: boolean;
@ -209,7 +207,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
this.INITIAL_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.INITIAL;
this.OUR_NUMBER = window.textsecure.storage.user.getNumber();
this.OUR_UUID = window.textsecure.storage.user.getUuid();
this.on('change', this.notifyRedux);
@ -364,7 +361,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.attributes,
findAndFormatContact,
ourConversationId,
this.OUR_NUMBER,
window.textsecure.storage.user.getNumber(),
this.OUR_UUID,
undefined,
undefined,
@ -1066,7 +1063,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
);
}
return this.OUR_NUMBER;
return window.textsecure.storage.user.getNumber();
}
getSourceDevice(): string | number | undefined {
@ -1280,11 +1277,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const quoteWithData = await loadQuoteData(this.get('quote'));
const previewWithData = await loadPreviewData(this.get('preview'));
const stickerWithData = await loadStickerData(this.get('sticker'));
const ourNumber = window.textsecure.storage.user.getNumber();
// Special-case the self-send case - we send only a sync message
if (
recipients.length === 1 &&
(recipients[0] === this.OUR_NUMBER || recipients[0] === this.OUR_UUID)
(recipients[0] === ourNumber || recipients[0] === this.OUR_UUID)
) {
const dataMessage = await window.textsecure.messaging.getDataMessage({
attachments,
@ -1434,9 +1432,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const quoteWithData = await loadQuoteData(this.get('quote'));
const previewWithData = await loadPreviewData(this.get('preview'));
const stickerWithData = await loadStickerData(this.get('sticker'));
const ourNumber = window.textsecure.storage.user.getNumber();
// Special-case the self-send case - we send only a sync message
if (identifier === this.OUR_NUMBER || identifier === this.OUR_UUID) {
if (identifier === ourNumber || identifier === this.OUR_UUID) {
const dataMessage = await window.textsecure.messaging.getDataMessage({
attachments,
body,

View File

@ -186,6 +186,11 @@ export async function toAccountRecord(
accountRecord.primarySendsSms = Boolean(primarySendsSms);
}
const accountE164 = window.storage.get('accountE164');
if (accountE164 !== undefined) {
accountRecord.e164 = accountE164;
}
const universalExpireTimer = getUniversalExpireTimer();
if (universalExpireTimer) {
accountRecord.universalExpireTimer = Number(universalExpireTimer);
@ -828,6 +833,7 @@ export async function mergeAccountRecord(
typingIndicators,
primarySendsSms,
universalExpireTimer,
e164: accountE164,
} = accountRecord;
window.storage.put('read-receipt-setting', Boolean(readReceipts));
@ -848,6 +854,11 @@ export async function mergeAccountRecord(
window.storage.put('primarySendsSms', primarySendsSms);
}
if (typeof accountE164 === 'string') {
window.storage.put('accountE164', accountE164);
window.storage.user.setNumber(accountE164);
}
setUniversalExpireTimer(universalExpireTimer || 0);
const PHONE_NUMBER_SHARING_MODE_ENUM =

View File

@ -3522,7 +3522,8 @@ async function hasUserInitiatedMessages(
'message-history-unsynced',
'keychange',
'group-v1-migration',
'universal-timer-notification'
'universal-timer-notification',
'change-number-notification'
)
) AND
json_extract(json, '$.expirationTimerUpdate') IS NULL
@ -4226,7 +4227,8 @@ async function getLastConversationActivity({
'message-history-unsynced',
'keychange',
'group-v1-migration',
'universal-timer-notification'
'universal-timer-notification',
'change-number-notification'
)
) AND
(
@ -4277,7 +4279,8 @@ async function getLastConversationPreview({
'verified-change',
'message-history-unsynced',
'group-v1-migration',
'universal-timer-notification'
'universal-timer-notification',
'change-number-notification'
)
) AND NOT
(

View File

@ -13,6 +13,7 @@ import {
import { TimelineItemType } from '../../components/conversation/TimelineItem';
import { PropsData } from '../../components/conversation/Message';
import { PropsData as TimerNotificationProps } from '../../components/conversation/TimerNotification';
import { PropsData as ChangeNumberNotificationProps } from '../../components/conversation/ChangeNumberNotification';
import { PropsData as SafetyNumberNotificationProps } from '../../components/conversation/SafetyNumberNotification';
import { PropsData as VerificationNotificationProps } from '../../components/conversation/VerificationNotification';
import { PropsDataType as GroupsV2Props } from '../../components/conversation/GroupV2Change';
@ -188,6 +189,12 @@ export function getPropsForBubble(
data: null,
};
}
if (isChangeNumberNotification(message)) {
return {
type: 'changeNumberNotification',
data: getPropsForChangeNumberNotification(message, conversationSelector),
};
}
if (isChatSessionRefreshed(message)) {
return {
type: 'chatSessionRefreshed',
@ -852,6 +859,24 @@ export function isUniversalTimerNotification(
return message.type === 'universal-timer-notification';
}
// Change Number Notification
export function isChangeNumberNotification(
message: MessageAttributesType
): boolean {
return message.type === 'change-number-notification';
}
function getPropsForChangeNumberNotification(
message: MessageAttributesType,
conversationSelector: GetConversationByIdType
): ChangeNumberNotificationProps {
return {
sender: conversationSelector(message.sourceUuid),
timestamp: message.sent_at,
};
}
// Chat Session Refreshed
export function isChatSessionRefreshed(

View File

@ -95,6 +95,7 @@ import {
ContactSyncEvent,
GroupEvent,
GroupSyncEvent,
EnvelopeEvent,
} from './messageReceiverEvents';
// TODO: remove once we move away from ArrayBuffers
@ -459,6 +460,11 @@ export default class MessageReceiver
handler: (ev: GroupSyncEvent) => void
): void;
public addEventListener(
name: 'envelope',
handler: (ev: EnvelopeEvent) => void
): void;
public addEventListener(name: string, handler: EventHandler): void {
return super.addEventListener(name, handler);
}
@ -981,8 +987,6 @@ export default class MessageReceiver
if (this.stoppingProcessing) {
return;
}
// No decryption is required for delivery receipts, so the decrypted field of
// the Unprocessed model will never be set
if (envelope.content) {
await this.innerHandleContentMessage(envelope, plaintext);

View File

@ -5,7 +5,11 @@
import { PublicKey } from '@signalapp/signal-client';
import { SignalService as Proto } from '../protobuf';
import { ProcessedDataMessage, ProcessedSent } from './Types.d';
import {
ProcessedEnvelope,
ProcessedDataMessage,
ProcessedSent,
} from './Types.d';
import type {
ModifiedContactDetails,
ModifiedGroupDetails,
@ -136,6 +140,12 @@ export class GroupSyncEvent extends Event {
}
}
export class EnvelopeEvent extends Event {
constructor(public readonly envelope: ProcessedEnvelope) {
super('envelope');
}
}
//
// Confirmable events below
//

View File

@ -3,6 +3,7 @@
import { WebAPICredentials } from '../Types.d';
import { strictAssert } from '../../util/assert';
import { StorageInterface } from '../../types/Storage.d';
import Helpers from '../Helpers';
@ -27,6 +28,25 @@ export class User {
window.log.info('storage.user: uuid and device id changed');
}
public async setNumber(number: string): Promise<void> {
if (this.getNumber() === number) {
return;
}
const deviceId = this.getDeviceId();
strictAssert(
deviceId !== undefined,
'Cannot update device number without knowing device id'
);
window.log.info('storage.user: number changed');
await this.storage.put('number_id', `${number}.${deviceId}`);
// Notify redux about phone number change
window.Whisper.events.trigger('userChanged');
}
public getNumber(): string | undefined {
const numberId = this.storage.get('number_id');
if (numberId === undefined) return undefined;

View File

@ -77,6 +77,9 @@ export type StorageAccessType = {
phoneNumberDiscoverability: PhoneNumberDiscoverability;
pinnedConversationIds: Array<string>;
primarySendsSms: boolean;
// Unlike `number_id` (which also includes device id) this field is only
// updated whenever we receive a new storage manifest
accountE164: string;
typingIndicators: boolean;
sealedSenderIndicators: boolean;
storageFetchComplete: boolean;