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

View file

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

View file

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

View file

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