Cache some volatile conversation properties

This commit is contained in:
Fedor Indutny 2022-12-22 16:13:23 -08:00 committed by GitHub
parent ba79595563
commit f92f81dfd6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 157 additions and 92 deletions

View file

@ -18,12 +18,13 @@ import type {
ConversationAttributesType, ConversationAttributesType,
ConversationLastProfileType, ConversationLastProfileType,
ConversationRenderInfoType, ConversationRenderInfoType,
LastMessageStatus,
MessageAttributesType, MessageAttributesType,
QuotedMessageType, QuotedMessageType,
SenderKeyInfoType, SenderKeyInfoType,
} from '../model-types.d'; } from '../model-types.d';
import { drop } from '../util/drop'; import { drop } from '../util/drop';
import { isShallowEqual } from '../util/isShallowEqual';
import { memoizeByThis } from '../util/memoizeByThis';
import { getInitials } from '../util/getInitials'; import { getInitials } from '../util/getInitials';
import { normalizeUuid } from '../util/normalizeUuid'; import { normalizeUuid } from '../util/normalizeUuid';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
@ -48,7 +49,10 @@ import type {
CallbackResultType, CallbackResultType,
PniSignatureMessageType, PniSignatureMessageType,
} from '../textsecure/Types.d'; } from '../textsecure/Types.d';
import type { ConversationType } from '../state/ducks/conversations'; import type {
ConversationType,
LastMessageType,
} from '../state/ducks/conversations';
import type { import type {
AvatarColorType, AvatarColorType,
ConversationColorType, ConversationColorType,
@ -78,7 +82,7 @@ import {
deriveAccessKey, deriveAccessKey,
} from '../Crypto'; } from '../Crypto';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import type { BodyRangesType } from '../types/Util'; import type { BodyRangesType, DraftBodyRangesType } from '../types/Util';
import { getTextWithMentions } from '../util/getTextWithMentions'; import { getTextWithMentions } from '../util/getTextWithMentions';
import { migrateColor } from '../util/migrateColor'; import { migrateColor } from '../util/migrateColor';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
@ -155,6 +159,9 @@ import { isMemberRequestingToJoin } from '../util/isMemberRequestingToJoin';
import { removePendingMember } from '../util/removePendingMember'; import { removePendingMember } from '../util/removePendingMember';
import { isMemberPending } from '../util/isMemberPending'; import { isMemberPending } from '../util/isMemberPending';
const EMPTY_ARRAY: Readonly<[]> = [];
const EMPTY_GROUP_COLLISIONS: GroupNameCollisionsWithIdsByTitle = {};
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -1722,7 +1729,15 @@ export class ConversationModel extends window.Backbone
}; };
try { try {
this.cachedProps = this.getProps(); const { oldCachedProps } = this;
const newCachedProps = this.getProps();
if (oldCachedProps && isShallowEqual(oldCachedProps, newCachedProps)) {
this.cachedProps = oldCachedProps;
} else {
this.cachedProps = newCachedProps;
}
return this.cachedProps; return this.cachedProps;
} finally { } finally {
this.format = oldFormat; this.format = oldFormat;
@ -1739,30 +1754,6 @@ export class ConversationModel extends window.Backbone
// 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()!;
let lastMessage:
| undefined
| {
status?: LastMessageStatus;
text: string;
author?: string;
deletedForEveryone: false;
}
| { deletedForEveryone: true };
if (this.get('lastMessageDeletedForEveryone')) {
lastMessage = { deletedForEveryone: true };
} else {
const lastMessageText = this.get('lastMessage');
if (lastMessageText) {
lastMessage = {
status: dropNull(this.get('lastMessageStatus')),
text: lastMessageText,
author: dropNull(this.get('lastMessageAuthor')),
deletedForEveryone: false,
};
}
}
const typingValues = Object.values(this.contactTypingTimers || {}); const typingValues = Object.values(this.contactTypingTimers || {});
const typingMostRecent = head(sortBy(typingValues, 'timestamp')); const typingMostRecent = head(sortBy(typingValues, 'timestamp'));
@ -1770,11 +1761,10 @@ export class ConversationModel extends window.Backbone
const timestamp = this.get('timestamp')!; const timestamp = this.get('timestamp')!;
const draftTimestamp = this.get('draftTimestamp'); const draftTimestamp = this.get('draftTimestamp');
const draftPreview = this.getDraftPreview(); const draftPreview = this.getDraftPreview();
const draftText = this.get('draft'); const draftText = dropNull(this.get('draft'));
const draftBodyRanges = this.get('draftBodyRanges'); const shouldShowDraft = Boolean(
const shouldShowDraft = (this.hasDraft() && this.hasDraft() && draftTimestamp && draftTimestamp >= timestamp
draftTimestamp && );
draftTimestamp >= timestamp) as boolean;
const inboxPosition = this.get('inbox_position'); const inboxPosition = this.get('inbox_position');
const messageRequestsEnabled = window.Signal.RemoteConfig.isEnabled( const messageRequestsEnabled = window.Signal.RemoteConfig.isEnabled(
'desktop.messageRequests' 'desktop.messageRequests'
@ -1831,7 +1821,7 @@ export class ConversationModel extends window.Backbone
), ),
areWeAdmin: this.areWeAdmin(), areWeAdmin: this.areWeAdmin(),
avatars: getAvatarData(this.attributes), avatars: getAvatarData(this.attributes),
badges: this.get('badges') || [], badges: this.get('badges') ?? EMPTY_ARRAY,
canChangeTimer: this.canChangeTimer(), canChangeTimer: this.canChangeTimer(),
canEditGroupInfo: this.canEditGroupInfo(), canEditGroupInfo: this.canEditGroupInfo(),
canAddNewMembers: this.canAddNewMembers(), canAddNewMembers: this.canAddNewMembers(),
@ -1844,7 +1834,7 @@ export class ConversationModel extends window.Backbone
customColor, customColor,
customColorId, customColorId,
discoveredUnregisteredAt: this.get('discoveredUnregisteredAt'), discoveredUnregisteredAt: this.get('discoveredUnregisteredAt'),
draftBodyRanges, draftBodyRanges: this.getDraftBodyRanges(),
draftPreview, draftPreview,
draftText, draftText,
familyName: this.get('profileFamilyName'), familyName: this.get('profileFamilyName'),
@ -1863,14 +1853,14 @@ export class ConversationModel extends window.Backbone
isUntrusted: this.isUntrusted(), isUntrusted: this.isUntrusted(),
isVerified: this.isVerified(), isVerified: this.isVerified(),
isFetchingUUID: this.isFetchingUUID, isFetchingUUID: this.isFetchingUUID,
lastMessage, lastMessage: this.getLastMessage(),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
lastUpdated: this.get('timestamp')!, lastUpdated: this.get('timestamp')!,
left: Boolean(this.get('left')), left: Boolean(this.get('left')),
markedUnread: this.get('markedUnread'), markedUnread: this.get('markedUnread'),
membersCount: this.getMembersCount(), membersCount: this.getMembersCount(),
memberships: this.getMemberships(), memberships: this.getMemberships(),
messageCount: this.get('messageCount') || 0, hasMessages: (this.get('messageCount') ?? 0) > 0,
pendingMemberships: this.getPendingMemberships(), pendingMemberships: this.getPendingMemberships(),
pendingApprovalMemberships: this.getPendingApprovalMemberships(), pendingApprovalMemberships: this.getPendingApprovalMemberships(),
bannedMemberships: this.getBannedMemberships(), bannedMemberships: this.getBannedMemberships(),
@ -1906,13 +1896,14 @@ export class ConversationModel extends window.Backbone
...(isDirectConversation(this.attributes) ...(isDirectConversation(this.attributes)
? { ? {
type: 'direct' as const, type: 'direct' as const,
sharedGroupNames: this.get('sharedGroupNames') || [], sharedGroupNames: this.get('sharedGroupNames') || EMPTY_ARRAY,
} }
: { : {
type: 'group' as const, type: 'group' as const,
acknowledgedGroupNameCollisions: acknowledgedGroupNameCollisions:
this.get('acknowledgedGroupNameCollisions') || {}, this.get('acknowledgedGroupNameCollisions') ||
sharedGroupNames: [], EMPTY_GROUP_COLLISIONS,
sharedGroupNames: EMPTY_ARRAY,
storySendMode: this.getGroupStorySendMode(), storySendMode: this.getGroupStorySendMode(),
}), }),
voiceNotePlaybackRate: this.get('voiceNotePlaybackRate'), voiceNotePlaybackRate: this.get('voiceNotePlaybackRate'),
@ -3611,20 +3602,44 @@ export class ConversationModel extends window.Backbone
return result; return result;
} }
private getMemberships(): Array<{ private getDraftBodyRanges = memoizeByThis(
uuid: UUIDStringType; (): DraftBodyRangesType | undefined => {
isAdmin: boolean; return this.get('draftBodyRanges');
}> {
if (!isGroupV2(this.attributes)) {
return [];
} }
);
const members = this.get('membersV2') || []; private getLastMessage = memoizeByThis((): LastMessageType | undefined => {
return members.map(member => ({ if (this.get('lastMessageDeletedForEveryone')) {
isAdmin: member.role === Proto.Member.Role.ADMINISTRATOR, return { deletedForEveryone: true };
uuid: member.uuid, }
})); const lastMessageText = this.get('lastMessage');
} if (!lastMessageText) {
return undefined;
}
return {
status: dropNull(this.get('lastMessageStatus')),
text: lastMessageText,
author: dropNull(this.get('lastMessageAuthor')),
deletedForEveryone: false,
};
});
private getMemberships = memoizeByThis(
(): ReadonlyArray<{
uuid: UUIDStringType;
isAdmin: boolean;
}> => {
if (!isGroupV2(this.attributes)) {
return EMPTY_ARRAY;
}
const members = this.get('membersV2') || [];
return members.map(member => ({
isAdmin: member.role === Proto.Member.Role.ADMINISTRATOR,
uuid: member.uuid,
}));
}
);
getGroupLink(): string | undefined { getGroupLink(): string | undefined {
if (!isGroupV2(this.attributes)) { if (!isGroupV2(this.attributes)) {
@ -3638,39 +3653,45 @@ export class ConversationModel extends window.Backbone
return window.Signal.Groups.buildGroupLink(this); return window.Signal.Groups.buildGroupLink(this);
} }
private getPendingMemberships(): Array<{ private getPendingMemberships = memoizeByThis(
addedByUserId?: UUIDStringType; (): ReadonlyArray<{
uuid: UUIDStringType; addedByUserId?: UUIDStringType;
}> { uuid: UUIDStringType;
if (!isGroupV2(this.attributes)) { }> => {
return []; if (!isGroupV2(this.attributes)) {
return EMPTY_ARRAY;
}
const members = this.get('pendingMembersV2') || [];
return members.map(member => ({
addedByUserId: member.addedByUserId,
uuid: member.uuid,
}));
} }
);
const members = this.get('pendingMembersV2') || []; private getPendingApprovalMemberships = memoizeByThis(
return members.map(member => ({ (): ReadonlyArray<{ uuid: UUIDStringType }> => {
addedByUserId: member.addedByUserId, if (!isGroupV2(this.attributes)) {
uuid: member.uuid, return EMPTY_ARRAY;
})); }
}
private getPendingApprovalMemberships(): Array<{ uuid: UUIDStringType }> { const members = this.get('pendingAdminApprovalV2') || [];
if (!isGroupV2(this.attributes)) { return members.map(member => ({
return []; uuid: member.uuid,
}));
} }
);
const members = this.get('pendingAdminApprovalV2') || []; private getBannedMemberships = memoizeByThis(
return members.map(member => ({ (): ReadonlyArray<UUIDStringType> => {
uuid: member.uuid, if (!isGroupV2(this.attributes)) {
})); return EMPTY_ARRAY;
} }
private getBannedMemberships(): Array<UUIDStringType> { return (this.get('bannedMembersV2') || []).map(member => member.uuid);
if (!isGroupV2(this.attributes)) {
return [];
} }
);
return (this.get('bannedMembersV2') || []).map(member => member.uuid);
}
getMembers( getMembers(
options: { includePendingMembers?: boolean } = {} options: { includePendingMembers?: boolean } = {}
@ -4132,7 +4153,7 @@ export class ConversationModel extends window.Backbone
const draftProperties = dontClearDraft const draftProperties = dontClearDraft
? {} ? {}
: { : {
draft: null, draft: '',
draftTimestamp: null, draftTimestamp: null,
lastMessage: model.getNotificationText(), lastMessage: model.getNotificationText(),
lastMessageAuthor: model.getAuthorText(), lastMessageAuthor: model.getAuthorText(),

View file

@ -155,6 +155,16 @@ export type MessageWithUIFieldsType = MessageAttributesType & {
export const ConversationTypes = ['direct', 'group'] as const; export const ConversationTypes = ['direct', 'group'] as const;
export type ConversationTypeType = typeof ConversationTypes[number]; export type ConversationTypeType = typeof ConversationTypes[number];
export type LastMessageType = Readonly<
| {
status?: LastMessageStatus;
text: string;
author?: string;
deletedForEveryone: false;
}
| { deletedForEveryone: true }
>;
export type ConversationType = { export type ConversationType = {
id: string; id: string;
uuid?: UUIDStringType; uuid?: UUIDStringType;
@ -197,18 +207,11 @@ export type ConversationType = {
timestamp?: number; timestamp?: number;
inboxPosition?: number; inboxPosition?: number;
left?: boolean; left?: boolean;
lastMessage?: lastMessage?: LastMessageType;
| {
status?: LastMessageStatus;
text: string;
author?: string;
deletedForEveryone: false;
}
| { deletedForEveryone: true };
markedUnread?: boolean; markedUnread?: boolean;
phoneNumber?: string; phoneNumber?: string;
membersCount?: number; membersCount?: number;
messageCount?: number; hasMessages?: boolean;
accessControlAddFromInviteLink?: number; accessControlAddFromInviteLink?: number;
accessControlAttributes?: number; accessControlAttributes?: number;
accessControlMembers?: number; accessControlMembers?: number;
@ -244,7 +247,7 @@ export type ConversationType = {
profileSharing?: boolean; profileSharing?: boolean;
shouldShowDraft?: boolean; shouldShowDraft?: boolean;
draftText?: string | null; draftText?: string;
draftBodyRanges?: DraftBodyRangesType; draftBodyRanges?: DraftBodyRangesType;
draftPreview?: string; draftPreview?: string;

View file

@ -1033,8 +1033,7 @@ export function isMissingRequiredProfileSharing(
doesConversationRequireIt && doesConversationRequireIt &&
!conversation.profileSharing && !conversation.profileSharing &&
window.Signal.RemoteConfig.isEnabled('desktop.mandatoryProfileSharing') && window.Signal.RemoteConfig.isEnabled('desktop.mandatoryProfileSharing') &&
conversation.messageCount && conversation.hasMessages
conversation.messageCount > 0
); );
} }

View file

@ -8,7 +8,9 @@ import type { ConversationType } from '../state/ducks/conversations';
import { isConversationNameKnown } from './isConversationNameKnown'; import { isConversationNameKnown } from './isConversationNameKnown';
import { isInSystemContacts } from './isInSystemContacts'; import { isInSystemContacts } from './isInSystemContacts';
export type GroupNameCollisionsWithIdsByTitle = Record<string, Array<string>>; export type GroupNameCollisionsWithIdsByTitle = Readonly<
Record<string, Array<string>>
>;
export type GroupNameCollisionsWithConversationsByTitle = Record< export type GroupNameCollisionsWithConversationsByTitle = Record<
string, string,
Array<ConversationType> Array<ConversationType>

20
ts/util/isShallowEqual.ts Normal file
View file

@ -0,0 +1,20 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export function isShallowEqual<Obj extends Record<string, unknown>>(
a: Obj,
b: Obj
): boolean {
const keys = Object.keys(a);
if (keys.length !== Object.keys(b).length) {
return false;
}
for (const key of keys) {
if (a[key] !== b[key]) {
return false;
}
}
return true;
}

20
ts/util/memoizeByThis.ts Normal file
View file

@ -0,0 +1,20 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isEqual } from 'lodash';
export function memoizeByThis<Owner extends Record<string, unknown>, Result>(
fn: () => Result
): () => Result {
const lastValueMap = new WeakMap<Owner, Result>();
return function memoizedFn(this: Owner): Result {
const lastValue = lastValueMap.get(this);
const newValue = fn();
if (lastValue !== undefined && isEqual(lastValue, newValue)) {
return lastValue;
}
lastValueMap.set(this, newValue);
return newValue;
};
}