Reliability fixes for conversation formatting and message send

This commit is contained in:
Scott Nonnenberg 2020-10-28 14:54:33 -07:00 committed by GitHub
parent fa2d300714
commit 8eea20ea91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 114 additions and 87 deletions

View file

@ -41,7 +41,6 @@ describe('Message', () => {
const fakeDataMessage = new ArrayBuffer(0); const fakeDataMessage = new ArrayBuffer(0);
const result = { const result = {
dataMessage: fakeDataMessage, dataMessage: fakeDataMessage,
discoveredIdentifierPairs: [],
}; };
const promise = Promise.resolve(result); const promise = Promise.resolve(result);
await message.send(promise); await message.send(promise);
@ -52,11 +51,7 @@ describe('Message', () => {
it('updates the `sent` attribute', async () => { it('updates the `sent` attribute', async () => {
const message = createMessage({ type: 'outgoing', source, sent: false }); const message = createMessage({ type: 'outgoing', source, sent: false });
await message.send( await message.send(Promise.resolve({}));
Promise.resolve({
discoveredIdentifierPairs: [],
})
);
assert.isTrue(message.get('sent')); assert.isTrue(message.get('sent'));
}); });
@ -69,11 +64,7 @@ describe('Message', () => {
callCount += 1; callCount += 1;
}); });
await message.send( await message.send(Promise.resolve({}));
Promise.resolve({
discoveredIdentifierPairs: [],
})
);
assert.strictEqual(callCount, 1); assert.strictEqual(callCount, 1);
}); });
@ -86,11 +77,7 @@ describe('Message', () => {
calls.push(args); calls.push(args);
}); });
await message.send( await message.send(Promise.resolve({}));
Promise.resolve({
discoveredIdentifierPairs: [],
})
);
assert.lengthOf(calls, 1); assert.lengthOf(calls, 1);
assert.strictEqual(calls[0][0], message); assert.strictEqual(calls[0][0], message);
@ -125,7 +112,6 @@ describe('Message', () => {
const result = { const result = {
errors: [new Error('baz qux')], errors: [new Error('baz qux')],
discoveredIdentifierPairs: [],
}; };
const promise = Promise.reject(result); const promise = Promise.reject(result);
await message.send(promise); await message.send(promise);

View file

@ -655,7 +655,14 @@ export class ConversationController {
const groups = await getAllGroupsInvolvingId(conversationId, { const groups = await getAllGroupsInvolvingId(conversationId, {
ConversationCollection: window.Whisper.ConversationCollection, ConversationCollection: window.Whisper.ConversationCollection,
}); });
return groups.map(group => this._conversations.add(group)); return groups.map(group => {
const existing = this.get(group.id);
if (existing) {
return existing;
}
return this._conversations.add(group);
});
} }
async loadPromise(): Promise<void> { async loadPromise(): Promise<void> {

View file

@ -24,9 +24,11 @@ story.addDecorator(storyFn => (
const createProps = (overrideProps: Partial<Props> = {}): Props => ({ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
...overrideProps, ...overrideProps,
i18n, i18n,
isAccepted: boolean( acceptedMessageRequest: boolean(
'isAccepted', 'acceptedMessageRequest',
overrideProps.isAccepted !== undefined ? overrideProps.isAccepted : true overrideProps.acceptedMessageRequest !== undefined
? overrideProps.acceptedMessageRequest
: true
), ),
isMe: boolean('isMe', overrideProps.isMe || false), isMe: boolean('isMe', overrideProps.isMe || false),
avatarPath: text('avatarPath', overrideProps.avatarPath || ''), avatarPath: text('avatarPath', overrideProps.avatarPath || ''),
@ -103,7 +105,7 @@ story.add('Typing Status', () => {
story.add('Message Request', () => { story.add('Message Request', () => {
const props = createProps({ const props = createProps({
isAccepted: false, acceptedMessageRequest: false,
lastMessage: { lastMessage: {
text: 'A Message', text: 'A Message',
status: 'delivered', status: 'delivered',

View file

@ -39,7 +39,7 @@ export type PropsData = {
unreadCount?: number; unreadCount?: number;
isSelected: boolean; isSelected: boolean;
isAccepted?: boolean; acceptedMessageRequest?: boolean;
draftPreview?: string; draftPreview?: string;
shouldShowDraft?: boolean; shouldShowDraft?: boolean;
@ -167,7 +167,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
const { const {
draftPreview, draftPreview,
i18n, i18n,
isAccepted, acceptedMessageRequest,
lastMessage, lastMessage,
muteExpiresAt, muteExpiresAt,
shouldShowDraft, shouldShowDraft,
@ -209,7 +209,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
{muteExpiresAt && Date.now() < muteExpiresAt && ( {muteExpiresAt && Date.now() < muteExpiresAt && (
<span className="module-conversation-list-item__muted" /> <span className="module-conversation-list-item__muted" />
)} )}
{!isAccepted ? ( {!acceptedMessageRequest ? (
<span className="module-conversation-list-item__message-request"> <span className="module-conversation-list-item__message-request">
{i18n('ConversationListItem--message-request')} {i18n('ConversationListItem--message-request')}
</span> </span>

View file

@ -70,7 +70,7 @@ const stories: Array<ConversationHeaderStory> = [
type: 'direct', type: 'direct',
id: '1', id: '1',
profileName: '🔥Flames🔥', profileName: '🔥Flames🔥',
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -85,7 +85,7 @@ const stories: Array<ConversationHeaderStory> = [
phoneNumber: '(202) 555-0002', phoneNumber: '(202) 555-0002',
type: 'direct', type: 'direct',
id: '2', id: '2',
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -100,7 +100,7 @@ const stories: Array<ConversationHeaderStory> = [
phoneNumber: '(202) 555-0002', phoneNumber: '(202) 555-0002',
type: 'direct', type: 'direct',
id: '2', id: '2',
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -115,7 +115,7 @@ const stories: Array<ConversationHeaderStory> = [
id: '3', id: '3',
title: '🔥Flames🔥', title: '🔥Flames🔥',
profileName: '🔥Flames🔥', profileName: '🔥Flames🔥',
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -127,7 +127,7 @@ const stories: Array<ConversationHeaderStory> = [
phoneNumber: '(202) 555-0011', phoneNumber: '(202) 555-0011',
type: 'direct', type: 'direct',
id: '11', id: '11',
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -141,7 +141,7 @@ const stories: Array<ConversationHeaderStory> = [
title: '(202) 555-0004', title: '(202) 555-0004',
type: 'direct', type: 'direct',
id: '4', id: '4',
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -165,7 +165,7 @@ const stories: Array<ConversationHeaderStory> = [
value: 10, value: 10,
}, },
], ],
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -179,7 +179,7 @@ const stories: Array<ConversationHeaderStory> = [
type: 'direct', type: 'direct',
id: '6', id: '6',
muteExpirationLabel: '10/18/3000, 11:11 AM', muteExpirationLabel: '10/18/3000, 11:11 AM',
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -211,7 +211,7 @@ const stories: Array<ConversationHeaderStory> = [
value: 10, value: 10,
}, },
], ],
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -237,7 +237,7 @@ const stories: Array<ConversationHeaderStory> = [
value: 10, value: 10,
}, },
], ],
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -257,7 +257,7 @@ const stories: Array<ConversationHeaderStory> = [
id: '7', id: '7',
type: 'direct', type: 'direct',
isMe: true, isMe: true,
isAccepted: true, acceptedMessageRequest: true,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },
@ -277,7 +277,7 @@ const stories: Array<ConversationHeaderStory> = [
id: '7', id: '7',
type: 'direct', type: 'direct',
isMe: false, isMe: false,
isAccepted: false, acceptedMessageRequest: false,
...actionProps, ...actionProps,
...housekeepingProps, ...housekeepingProps,
}, },

View file

@ -31,7 +31,7 @@ export interface PropsDataType {
type: 'direct' | 'group'; type: 'direct' | 'group';
title: string; title: string;
isAccepted?: boolean; acceptedMessageRequest?: boolean;
isVerified?: boolean; isVerified?: boolean;
isMe?: boolean; isMe?: boolean;
isArchived?: boolean; isArchived?: boolean;
@ -305,7 +305,7 @@ export class ConversationHeader extends React.Component<PropsType> {
const { const {
disableTimerChanges, disableTimerChanges,
i18n, i18n,
isAccepted, acceptedMessageRequest,
isMe, isMe,
isPinned, isPinned,
type, type,
@ -388,7 +388,7 @@ export class ConversationHeader extends React.Component<PropsType> {
{i18n('showSafetyNumber')} {i18n('showSafetyNumber')}
</MenuItem> </MenuItem>
) : null} ) : null}
{!isGroup && isAccepted ? ( {!isGroup && acceptedMessageRequest ? (
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem> <MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
) : null} ) : null}
{isArchived ? ( {isArchived ? (

2
ts/model-types.d.ts vendored
View file

@ -147,7 +147,7 @@ export type ConversationAttributesType = {
draftTimestamp: number | null; draftTimestamp: number | null;
inbox_position: number; inbox_position: number;
isPinned: boolean; isPinned: boolean;
lastMessageDeletedForEveryone: unknown; lastMessageDeletedForEveryone: boolean;
lastMessageStatus: LastMessageStatus | null; lastMessageStatus: LastMessageStatus | null;
messageCount: number; messageCount: number;
messageCountBeforeMessageRequests: number; messageCountBeforeMessageRequests: number;

View file

@ -80,6 +80,8 @@ export class ConversationModel extends window.Backbone.Model<
cachedProps?: ConversationType | null; cachedProps?: ConversationType | null;
oldCachedProps?: ConversationType | null;
contactTypingTimers?: Record< contactTypingTimers?: Record<
string, string,
{ senderId: string; timer: NodeJS.Timer } { senderId: string; timer: NodeJS.Timer }
@ -235,6 +237,9 @@ export class ConversationModel extends window.Backbone.Model<
// We clear our cached props whenever we change so that the next call to format() will // We clear our cached props whenever we change so that the next call to format() will
// result in refresh via a getProps() call. See format() below. // result in refresh via a getProps() call. See format() below.
this.on('change', () => { this.on('change', () => {
if (this.cachedProps) {
this.oldCachedProps = this.cachedProps;
}
this.cachedProps = null; this.cachedProps = null;
}); });
} }
@ -1020,12 +1025,38 @@ export class ConversationModel extends window.Backbone.Model<
} }
format(): ConversationType { format(): ConversationType {
this.cachedProps = this.cachedProps || this.getProps(); if (this.cachedProps) {
return this.cachedProps;
}
const oldFormat = this.format;
// We don't want to crash or have an infinite loop if we loop back into this function
// again. We'll log a warning and returned old cached props or throw an error.
this.format = () => {
const { stack } = new Error('for stack');
window.log.warn(
`Conversation.format()/${this.idForLogging()} reentrant call! ${stack}`
);
if (!this.oldCachedProps) {
throw new Error(
`Conversation.format()/${this.idForLogging()} reentrant call, no old cached props!`
);
}
return this.oldCachedProps;
};
this.cachedProps = this.getProps();
this.format = oldFormat;
return this.cachedProps; return this.cachedProps;
} }
getProps(): ConversationType { // Note: this should never be called directly. Use conversation.format() instead, which
// maintains a cache, and protects against reentrant calls.
// Note: When writing code inside this function, do not call .format() on a conversation
// unless you are sure that it's not this very same conversation.
private getProps(): ConversationType {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const color = this.getColor()!; const color = this.getColor()!;
@ -1060,7 +1091,7 @@ export class ConversationModel extends window.Backbone.Model<
// TODO: DESKTOP-720 // TODO: DESKTOP-720
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
const result = { const result: ConversationType = {
id: this.id, id: this.id,
uuid: this.get('uuid'), uuid: this.get('uuid'),
e164: this.get('e164'), e164: this.get('e164'),
@ -1077,7 +1108,6 @@ export class ConversationModel extends window.Backbone.Model<
firstName: this.get('profileName')!, firstName: this.get('profileName')!,
groupVersion, groupVersion,
inboxPosition, inboxPosition,
isAccepted: this.getAccepted(),
isArchived: this.get('isArchived')!, isArchived: this.get('isArchived')!,
isBlocked: this.isBlocked(), isBlocked: this.isBlocked(),
isMe: this.isMe(), isMe: this.isMe(),
@ -1104,9 +1134,17 @@ export class ConversationModel extends window.Backbone.Model<
timestamp, timestamp,
title: this.getTitle()!, title: this.getTitle()!,
type: (this.isPrivate() ? 'direct' : 'group') as ConversationTypeType, type: (this.isPrivate() ? 'direct' : 'group') as ConversationTypeType,
typingContact: typingContact ? typingContact.format() : null,
unreadCount: this.get('unreadCount')! || 0, unreadCount: this.get('unreadCount')! || 0,
}; };
if (typingContact) {
// We don't want to call .format() on our own conversation
if (typingContact.id === this.id) {
result.typingContact = result;
} else {
result.typingContact = typingContact.format();
}
}
/* eslint-enable @typescript-eslint/no-non-null-assertion */ /* eslint-enable @typescript-eslint/no-non-null-assertion */
return result; return result;
@ -2710,8 +2748,7 @@ export class ConversationModel extends window.Backbone.Model<
if (result) { if (result) {
await this.handleMessageSendResult( await this.handleMessageSendResult(
result.failoverIdentifiers, result.failoverIdentifiers,
result.unidentifiedDeliveries, result.unidentifiedDeliveries
result.discoveredIdentifierPairs
); );
} }
return result; return result;
@ -2721,8 +2758,7 @@ export class ConversationModel extends window.Backbone.Model<
if (result) { if (result) {
await this.handleMessageSendResult( await this.handleMessageSendResult(
result.failoverIdentifiers, result.failoverIdentifiers,
result.unidentifiedDeliveries, result.unidentifiedDeliveries
result.discoveredIdentifierPairs
); );
} }
throw result; throw result;
@ -2732,23 +2768,8 @@ export class ConversationModel extends window.Backbone.Model<
async handleMessageSendResult( async handleMessageSendResult(
failoverIdentifiers: Array<string> | undefined, failoverIdentifiers: Array<string> | undefined,
unidentifiedDeliveries: Array<string> | undefined, unidentifiedDeliveries: Array<string> | undefined
discoveredIdentifierPairs:
| Array<{
uuid: string | null;
e164: string | null;
}>
| undefined
): Promise<void> { ): Promise<void> {
(discoveredIdentifierPairs || []).forEach(item => {
const { uuid, e164 } = item;
window.ConversationController.ensureContactIds({
uuid,
e164,
highTrust: true,
});
});
await Promise.all( await Promise.all(
(failoverIdentifiers || []).map(async identifier => { (failoverIdentifiers || []).map(async identifier => {
const conversation = window.ConversationController.get(identifier); const conversation = window.ConversationController.get(identifier);
@ -4246,15 +4267,37 @@ window.Whisper.ConversationCollection = window.Backbone.Collection.extend({
this._byGroupId = Object.create(null); this._byGroupId = Object.create(null);
}, },
add(...models: Array<WhatIsThis>) { add(data: WhatIsThis | Array<WhatIsThis>) {
const result = window.Backbone.Collection.prototype.add.apply( let hydratedData;
this,
models as WhatIsThis // First, we need to ensure that the data we're working with is Conversation models
if (Array.isArray(data)) {
hydratedData = [];
for (let i = 0, max = data.length; i < max; i += 1) {
const item = data[i];
// We create a new model if it's not already a model
if (!item.get) {
hydratedData.push(new Whisper.Conversation(item));
} else {
hydratedData.push(item);
}
}
} else if (!data.get) {
hydratedData = new Whisper.Conversation(data);
} else {
hydratedData = data;
}
// Next, we update our lookups first to prevent infinite loops on the 'add' event
this.generateLookups(
Array.isArray(hydratedData) ? hydratedData : [hydratedData]
); );
this.generateLookups(Array.isArray(result) ? result.slice(0) : [result]); // Lastly, we fire off the add events related to this change
window.Backbone.Collection.prototype.add.call(this, hydratedData);
return result; return hydratedData;
}, },
/** /**

View file

@ -54,6 +54,7 @@ export type ConversationType = {
lastMessage?: { lastMessage?: {
status: LastMessageStatus; status: LastMessageStatus;
text: string; text: string;
deletedForEveryone?: boolean;
}; };
phoneNumber?: string; phoneNumber?: string;
membersCount?: number; membersCount?: number;
@ -76,6 +77,7 @@ export type ConversationType = {
draftText?: string | null; draftText?: string | null;
draftPreview?: string; draftPreview?: string;
sharedGroupNames?: Array<string>;
groupVersion?: 1 | 2; groupVersion?: 1 | 2;
isMissingMandatoryProfileSharing?: boolean; isMissingMandatoryProfileSharing?: boolean;
messageRequestsEnabled?: boolean; messageRequestsEnabled?: boolean;

View file

@ -53,11 +53,6 @@ export default class OutgoingMessage {
unidentifiedDeliveries: Array<unknown>; unidentifiedDeliveries: Array<unknown>;
discoveredIdentifierPairs: Array<{
e164: string;
uuid: string;
}>;
sendMetadata?: SendMetadataType; sendMetadata?: SendMetadataType;
senderCertificate?: ArrayBuffer; senderCertificate?: ArrayBuffer;
@ -93,7 +88,6 @@ export default class OutgoingMessage {
this.successfulIdentifiers = []; this.successfulIdentifiers = [];
this.failoverIdentifiers = []; this.failoverIdentifiers = [];
this.unidentifiedDeliveries = []; this.unidentifiedDeliveries = [];
this.discoveredIdentifierPairs = [];
const { sendMetadata, senderCertificate, online } = options; const { sendMetadata, senderCertificate, online } = options;
this.sendMetadata = sendMetadata; this.sendMetadata = sendMetadata;
@ -109,7 +103,6 @@ export default class OutgoingMessage {
failoverIdentifiers: this.failoverIdentifiers, failoverIdentifiers: this.failoverIdentifiers,
errors: this.errors, errors: this.errors,
unidentifiedDeliveries: this.unidentifiedDeliveries, unidentifiedDeliveries: this.unidentifiedDeliveries,
discoveredIdentifierPairs: this.discoveredIdentifierPairs,
}); });
} }
} }
@ -621,9 +614,10 @@ export default class OutgoingMessage {
]); ]);
const uuid = lookup[identifier]; const uuid = lookup[identifier];
if (uuid) { if (uuid) {
this.discoveredIdentifierPairs.push({ window.ConversationController.ensureContactIds({
uuid, uuid,
e164: identifier, e164: identifier,
highTrust: true,
}); });
identifier = uuid; identifier = uuid;
} else { } else {

View file

@ -78,10 +78,6 @@ export type CallbackResultType = {
errors?: Array<CustomError>; errors?: Array<CustomError>;
unidentifiedDeliveries?: Array<any>; unidentifiedDeliveries?: Array<any>;
dataMessage?: ArrayBuffer; dataMessage?: ArrayBuffer;
discoveredIdentifierPairs: Array<{
e164: string;
uuid: string | null;
}>;
}; };
type PreviewType = { type PreviewType = {
@ -1383,7 +1379,6 @@ export default class MessageSender {
if (identifiers.length === 0) { if (identifiers.length === 0) {
return Promise.resolve({ return Promise.resolve({
dataMessage: proto.toArrayBuffer(), dataMessage: proto.toArrayBuffer(),
discoveredIdentifierPairs: [],
errors: [], errors: [],
failoverIdentifiers: [], failoverIdentifiers: [],
successfulIdentifiers: [], successfulIdentifiers: [],
@ -1657,7 +1652,6 @@ export default class MessageSender {
errors: [], errors: [],
unidentifiedDeliveries: [], unidentifiedDeliveries: [],
dataMessage: await this.getMessageProtoObj(attrs), dataMessage: await this.getMessageProtoObj(attrs),
discoveredIdentifierPairs: [],
}); });
} }
@ -1731,7 +1725,6 @@ export default class MessageSender {
errors: [], errors: [],
unidentifiedDeliveries: [], unidentifiedDeliveries: [],
dataMessage: await this.getMessageProtoObj(attrs), dataMessage: await this.getMessageProtoObj(attrs),
discoveredIdentifierPairs: [],
}); });
} }