From 68a458ec4af8070f3b1cb8868cd767dccb647095 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 25 Jan 2022 09:44:45 -0800 Subject: [PATCH] Honor preferContactAvatars field on AccountRecord --- protos/SignalStorage.proto | 1 + ts/ConversationController.ts | 31 ++++++++++ ts/background.ts | 6 +- ts/models/conversations.ts | 5 +- ts/services/storageRecordOps.ts | 15 +++++ .../util/areArraysMatchingSets_test.ts | 57 +++++++++++++++++++ ts/textsecure/MessageReceiver.ts | 27 ++++++++- ts/types/Storage.d.ts | 1 + ts/util/areArraysMatchingSets.ts | 24 ++++++++ 9 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 ts/test-both/util/areArraysMatchingSets_test.ts create mode 100644 ts/util/areArraysMatchingSets.ts diff --git a/protos/SignalStorage.proto b/protos/SignalStorage.proto index a43162c49..e270b2833 100644 --- a/protos/SignalStorage.proto +++ b/protos/SignalStorage.proto @@ -133,6 +133,7 @@ message AccountRecord { optional PhoneNumberSharingMode phoneNumberSharingMode = 12; optional bool notDiscoverableByPhoneNumber = 13; repeated PinnedConversation pinnedConversations = 14; + optional bool preferContactAvatars = 15; optional uint32 universalExpireTimer = 17; optional bool primarySendsSms = 18; optional string e164 = 19; diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index 887e0d696..c3995e5be 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -20,6 +20,7 @@ import { UUID, isValidUuid } from './types/UUID'; import { Address } from './types/Address'; import { QualifiedAddress } from './types/QualifiedAddress'; import * as log from './logging/log'; +import { sleep } from './util/sleep'; const MAX_MESSAGE_BODY_LENGTH = 64 * 1024; @@ -784,6 +785,36 @@ export class ConversationController { return this._initialPromise; } + // A number of things outside conversation.attributes affect conversation re-rendering. + // If it's scoped to a given conversation, it's easy to trigger('change'). There are + // important values in storage and the storage service which change rendering pretty + // radically, so this function is necessary to force regeneration of props. + async forceRerender(): Promise { + let count = 0; + const conversations = this._conversations.models.slice(); + log.info( + `forceRerender: Starting to loop through ${conversations.length} conversations` + ); + + for (let i = 0, max = conversations.length; i < max; i += 1) { + const conversation = conversations[i]; + + if (conversation.cachedProps) { + conversation.oldCachedProps = conversation.cachedProps; + conversation.cachedProps = null; + + conversation.trigger('props-change', conversation, false); + count += 1; + } + + if (count % 10 === 0) { + // eslint-disable-next-line no-await-in-loop + await sleep(300); + } + } + log.info(`forceRerender: Updated ${count} conversations`); + } + onConvoOpenStart(conversationId: string): void { this._conversationOpenStart.set(conversationId, Date.now()); } diff --git a/ts/background.ts b/ts/background.ts index 6a185691f..38ee05958 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -684,8 +684,10 @@ export async function startApp(): Promise { } if ( - window.isBeforeVersion(lastVersion, 'v1.36.0-beta.1') && - window.isAfterVersion(lastVersion, 'v1.35.0-beta.1') + (window.isBeforeVersion(lastVersion, 'v1.36.0-beta.1') && + window.isAfterVersion(lastVersion, 'v1.35.0-beta.1')) || + // 5.30 introduced understanding of new storage service AccountRecord fields + window.isBeforeVersion(lastVersion, 'v5.30.0-alpha') ) { await window.Signal.Services.eraseAllStorageServiceState(); } diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 8de238173..de3be25e1 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -4896,7 +4896,10 @@ export class ConversationModel extends window.Backbone } private getAvatarPath(): undefined | string { - const avatar = isMe(this.attributes) + const shouldShowProfileAvatar = + isMe(this.attributes) || + window.storage.get('preferContactAvatars') === false; + const avatar = shouldShowProfileAvatar ? this.get('profileAvatar') || this.get('avatar') : this.get('avatar') || this.get('profileAvatar'); return avatar?.path || undefined; diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index be640b9db..8b3042632 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -182,6 +182,11 @@ export async function toAccountRecord( ); accountRecord.linkPreviews = Boolean(window.Events.getLinkPreviewSetting()); + const preferContactAvatars = window.storage.get('preferContactAvatars'); + if (preferContactAvatars !== undefined) { + accountRecord.preferContactAvatars = Boolean(preferContactAvatars); + } + const primarySendsSms = window.storage.get('primarySendsSms'); if (primarySendsSms !== undefined) { accountRecord.primarySendsSms = Boolean(primarySendsSms); @@ -855,6 +860,7 @@ export async function mergeAccountRecord( readReceipts, sealedSenderIndicators, typingIndicators, + preferContactAvatars, primarySendsSms, universalExpireTimer, e164: accountE164, @@ -878,6 +884,15 @@ export async function mergeAccountRecord( window.storage.put('linkPreviews', linkPreviews); } + if (typeof preferContactAvatars === 'boolean') { + const previous = window.storage.get('preferContactAvatars'); + window.storage.put('preferContactAvatars', preferContactAvatars); + + if (Boolean(previous) !== Boolean(preferContactAvatars)) { + window.ConversationController.forceRerender(); + } + } + if (typeof primarySendsSms === 'boolean') { window.storage.put('primarySendsSms', primarySendsSms); } diff --git a/ts/test-both/util/areArraysMatchingSets_test.ts b/ts/test-both/util/areArraysMatchingSets_test.ts new file mode 100644 index 000000000..e832d2fc5 --- /dev/null +++ b/ts/test-both/util/areArraysMatchingSets_test.ts @@ -0,0 +1,57 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import { areArraysMatchingSets } from '../../util/areArraysMatchingSets'; + +describe('areArraysMatchingSets', () => { + it('returns true if arrays are both empty', () => { + const left: Array = []; + const right: Array = []; + + assert.isTrue(areArraysMatchingSets(left, right)); + }); + + it('returns true if arrays are equal', () => { + const left = [1, 2, 3]; + const right = [1, 2, 3]; + + assert.isTrue(areArraysMatchingSets(left, right)); + }); + + it('returns true if arrays are equal but out of order', () => { + const left = [1, 2, 3]; + const right = [3, 1, 2]; + + assert.isTrue(areArraysMatchingSets(left, right)); + }); + + it('returns true if arrays are equal but one has duplicates', () => { + const left = [1, 2, 3, 1]; + const right = [1, 2, 3]; + + assert.isTrue(areArraysMatchingSets(left, right)); + }); + + it('returns false if first array has missing elements', () => { + const left = [1, 2]; + const right = [1, 2, 3]; + + assert.isFalse(areArraysMatchingSets(left, right)); + }); + + it('returns false if second array has missing elements', () => { + const left = [1, 2, 3]; + const right = [1, 2]; + + assert.isFalse(areArraysMatchingSets(left, right)); + }); + + it('returns false if second array is empty', () => { + const left = [1, 2, 3]; + const right: Array = []; + + assert.isFalse(areArraysMatchingSets(left, right)); + }); +}); diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 19d7670c5..dfa2dc99e 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -105,6 +105,7 @@ import { GroupSyncEvent, } from './messageReceiverEvents'; import * as log from '../logging/log'; +import { areArraysMatchingSets } from '../util/areArraysMatchingSets'; const GROUPV1_ID_LENGTH = 16; const GROUPV2_ID_LENGTH = 32; @@ -2629,19 +2630,32 @@ export default class MessageReceiver envelope: ProcessedEnvelope, blocked: Proto.SyncMessage.IBlocked ): Promise { + let changed = false; + if (blocked.numbers) { + const previous = this.storage.get('blocked', []); log.info('handleBlocked: Blocking these numbers:', blocked.numbers); await this.storage.put('blocked', blocked.numbers); + + if (!areArraysMatchingSets(previous, blocked.numbers)) { + changed = true; + } } if (blocked.uuids) { + const previous = this.storage.get('blocked-uuids', []); const uuids = blocked.uuids.map((uuid, index) => { return normalizeUuid(uuid, `handleBlocked.uuids.${index}`); }); log.info('handleBlocked: Blocking these uuids:', uuids); await this.storage.put('blocked-uuids', uuids); + + if (!areArraysMatchingSets(previous, uuids)) { + changed = true; + } } if (blocked.groupIds) { + const previous = this.storage.get('blocked-groups', []); const groupV1Ids: Array = []; const groupIds: Array = []; @@ -2661,10 +2675,21 @@ export default class MessageReceiver 'v1:', groupV1Ids.map(groupId => `group(${groupId})`) ); - await this.storage.put('blocked-groups', [...groupIds, ...groupV1Ids]); + + const ids = [...groupIds, ...groupV1Ids]; + await this.storage.put('blocked-groups', ids); + + if (!areArraysMatchingSets(previous, ids)) { + changed = true; + } } this.removeFromCache(envelope); + + if (changed) { + log.info('handleBlocked: Block list changed, forcing re-render.'); + window.ConversationController.forceRerender(); + } } private isBlocked(number: string): boolean { diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts index d05b50923..30927b3d4 100644 --- a/ts/types/Storage.d.ts +++ b/ts/types/Storage.d.ts @@ -94,6 +94,7 @@ export type StorageAccessType = { phoneNumberSharingMode: PhoneNumberSharingMode; phoneNumberDiscoverability: PhoneNumberDiscoverability; pinnedConversationIds: Array; + preferContactAvatars: boolean; primarySendsSms: boolean; // Unlike `number_id` (which also includes device id) this field is only // updated whenever we receive a new storage manifest diff --git a/ts/util/areArraysMatchingSets.ts b/ts/util/areArraysMatchingSets.ts new file mode 100644 index 000000000..f428413ee --- /dev/null +++ b/ts/util/areArraysMatchingSets.ts @@ -0,0 +1,24 @@ +// Copyright 2020-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export function areArraysMatchingSets( + left: Array, + right: Array +): boolean { + const leftSet = new Set(left); + const rightSet = new Set(right); + + for (const item of leftSet) { + if (!rightSet.has(item)) { + return false; + } + } + + for (const item of rightSet) { + if (!leftSet.has(item)) { + return false; + } + } + + return true; +}