Import/export avatar colors
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -240,7 +240,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'signalapp/Signal-Message-Backup-Tests'
|
||||
ref: '2e22808478e08c72e11a7483900c9af6c9b1acf4'
|
||||
ref: 'a743fbf8e3adc2f1a700577dd8a470beff60db3f'
|
||||
path: 'backup-integration-tests'
|
||||
|
||||
- run: xvfb-run --auto-servernum pnpm run test-electron
|
||||
|
@@ -118,7 +118,7 @@
|
||||
"@react-aria/utils": "3.25.3",
|
||||
"@react-spring/web": "9.7.5",
|
||||
"@signalapp/better-sqlite3": "9.0.13",
|
||||
"@signalapp/libsignal-client": "0.67.0",
|
||||
"@signalapp/libsignal-client": "0.67.3",
|
||||
"@signalapp/quill-cjs": "2.1.2",
|
||||
"@signalapp/ringrtc": "2.50.1",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -125,8 +125,8 @@ importers:
|
||||
specifier: 9.0.13
|
||||
version: 9.0.13
|
||||
'@signalapp/libsignal-client':
|
||||
specifier: 0.67.0
|
||||
version: 0.67.0
|
||||
specifier: 0.67.3
|
||||
version: 0.67.3
|
||||
'@signalapp/quill-cjs':
|
||||
specifier: 2.1.2
|
||||
version: 2.1.2
|
||||
@@ -2532,8 +2532,8 @@ packages:
|
||||
'@signalapp/libsignal-client@0.60.2':
|
||||
resolution: {integrity: sha512-tU4kNP/yCwkFntb2ahXOSQJtzdy+YifAB2yv5hw0qyKSidRHLn6bYiz4Zo2tjxLDRoBLAUxCRsQramStiqNZdA==}
|
||||
|
||||
'@signalapp/libsignal-client@0.67.0':
|
||||
resolution: {integrity: sha512-K0MdgOQMqQkeXM9bBRa4FRw+q/IE6a+IICgw7wUwaO7y11IWLCRfaam/hNtHS1uzwPx6D1Ebo0pwKO9kHeShPw==}
|
||||
'@signalapp/libsignal-client@0.67.3':
|
||||
resolution: {integrity: sha512-GIiXJMqiIByPZbomytoYQcQLJ3pNgHBCjt5BvlE/3rkrmNwyXW1UVqszX6/WfZi91aqUapO4+7Op+8JCbDGRWA==}
|
||||
|
||||
'@signalapp/mock-server@11.0.0':
|
||||
resolution: {integrity: sha512-JHEqdjXvWcXyLJ90OtaTIQVtizH/w+rmnPplNmHuUiDZIQIEvE3lRyZFyIvgVTNg29lQWs/Gw/o+hICerdOChQ==}
|
||||
@@ -12260,7 +12260,7 @@ snapshots:
|
||||
type-fest: 4.26.1
|
||||
uuid: 8.3.2
|
||||
|
||||
'@signalapp/libsignal-client@0.67.0':
|
||||
'@signalapp/libsignal-client@0.67.3':
|
||||
dependencies:
|
||||
node-gyp-build: 4.8.4
|
||||
type-fest: 4.26.1
|
||||
|
@@ -118,6 +118,7 @@ message AccountData {
|
||||
reserved /*backupsSubscriberData*/ 8; // A deprecated format
|
||||
AccountSettings accountSettings = 9;
|
||||
IAPSubscriberData backupsSubscriberData = 10;
|
||||
string svrPin = 11;
|
||||
}
|
||||
|
||||
message Recipient {
|
||||
@@ -133,6 +134,30 @@ message Recipient {
|
||||
}
|
||||
}
|
||||
|
||||
// If unset - computed as the value of the first byte of SHA-256(msg=CONTACT_ID)
|
||||
// modulo the count of colors. Once set the avatar color for a recipient is
|
||||
// never recomputed or changed.
|
||||
//
|
||||
// `CONTACT_ID` is the first available identifier from the list:
|
||||
// - ServiceIdToBinary(ACI)
|
||||
// - E164
|
||||
// - ServiceIdToBinary(PNI)
|
||||
// - Group Id
|
||||
enum AvatarColor {
|
||||
A100 = 0;
|
||||
A110 = 1;
|
||||
A120 = 2;
|
||||
A130 = 3;
|
||||
A140 = 4;
|
||||
A150 = 5;
|
||||
A160 = 6;
|
||||
A170 = 7;
|
||||
A180 = 8;
|
||||
A190 = 9;
|
||||
A200 = 10;
|
||||
A210 = 11;
|
||||
}
|
||||
|
||||
message Contact {
|
||||
enum IdentityState {
|
||||
DEFAULT = 0; // A valid value -- indicates unset by the user
|
||||
@@ -181,6 +206,7 @@ message Contact {
|
||||
string systemGivenName = 18;
|
||||
string systemFamilyName = 19;
|
||||
string systemNickname = 20;
|
||||
optional AvatarColor avatarColor = 21;
|
||||
}
|
||||
|
||||
message Group {
|
||||
@@ -196,6 +222,7 @@ message Group {
|
||||
StorySendMode storySendMode = 4;
|
||||
GroupSnapshot snapshot = 5;
|
||||
bool blocked = 6;
|
||||
optional AvatarColor avatarColor = 7;
|
||||
|
||||
// These are simply plaintext copies of the groups proto from Groups.proto.
|
||||
// They should be kept completely in-sync with Groups.proto.
|
||||
@@ -275,7 +302,9 @@ message Group {
|
||||
}
|
||||
}
|
||||
|
||||
message Self {}
|
||||
message Self {
|
||||
optional AvatarColor avatarColor = 1;
|
||||
}
|
||||
|
||||
message ReleaseNotes {}
|
||||
|
||||
|
@@ -73,6 +73,30 @@ message StorageRecord {
|
||||
}
|
||||
}
|
||||
|
||||
// If unset - computed as the value of the first byte of SHA-256(msg=CONTACT_ID)
|
||||
// modulo the count of colors. Once set the avatar color for a recipient is
|
||||
// never recomputed or changed.
|
||||
//
|
||||
// `CONTACT_ID` is the first available identifier from the list:
|
||||
// - ServiceIdToBinary(ACI)
|
||||
// - E164
|
||||
// - ServiceIdToBinary(PNI)
|
||||
// - Group Id
|
||||
enum AvatarColor {
|
||||
A100 = 0;
|
||||
A110 = 1;
|
||||
A120 = 2;
|
||||
A130 = 3;
|
||||
A140 = 4;
|
||||
A150 = 5;
|
||||
A160 = 6;
|
||||
A170 = 7;
|
||||
A180 = 8;
|
||||
A190 = 9;
|
||||
A200 = 10;
|
||||
A210 = 11;
|
||||
}
|
||||
|
||||
message ContactRecord {
|
||||
enum IdentityState {
|
||||
DEFAULT = 0;
|
||||
@@ -108,6 +132,7 @@ message ContactRecord {
|
||||
optional bool pniSignatureVerified = 21;
|
||||
optional Name nickname = 22;
|
||||
optional string note = 23;
|
||||
optional AvatarColor avatarColor = 24;
|
||||
}
|
||||
|
||||
message GroupV1Record {
|
||||
@@ -136,6 +161,7 @@ message GroupV2Record {
|
||||
optional bool hideStory = 8;
|
||||
reserved /* storySendEnabled */ 9; // removed
|
||||
optional StorySendMode storySendMode = 10;
|
||||
optional AvatarColor avatarColor = 11;
|
||||
}
|
||||
|
||||
message AccountRecord {
|
||||
@@ -229,6 +255,7 @@ message AccountRecord {
|
||||
// See zkgroup for integer particular values
|
||||
optional uint64 backupTier = 40;
|
||||
optional IAPSubscriberData backupSubscriberData = 41;
|
||||
optional AvatarColor avatarColor = 42;
|
||||
}
|
||||
|
||||
message StoryDistributionListRecord {
|
||||
|
34
ts/Crypto.ts
34
ts/Crypto.ts
@@ -3,20 +3,22 @@
|
||||
|
||||
import { Buffer } from 'buffer';
|
||||
import Long from 'long';
|
||||
import { Aci, HKDF } from '@signalapp/libsignal-client';
|
||||
import { sample } from 'lodash';
|
||||
import { Aci, Pni, HKDF } from '@signalapp/libsignal-client';
|
||||
import { AccountEntropyPool } from '@signalapp/libsignal-client/dist/AccountKeys';
|
||||
|
||||
import * as Bytes from './Bytes';
|
||||
import { Crypto } from './context/Crypto';
|
||||
import { calculateAgreement, generateKeyPair } from './Curve';
|
||||
import { HashType, CipherType } from './types/Crypto';
|
||||
import { AVATAR_COLOR_COUNT, AvatarColors } from './types/Colors';
|
||||
import { ProfileDecryptError } from './types/errors';
|
||||
import { getBytesSubarray } from './util/uuidToBytes';
|
||||
import { logPadSize } from './util/logPadding';
|
||||
import { Environment, getEnvironment } from './environment';
|
||||
import { toWebSafeBase64 } from './util/webSafeBase64';
|
||||
|
||||
import type { AciString } from './types/ServiceId';
|
||||
import type { AciString, PniString } from './types/ServiceId';
|
||||
|
||||
export { HashType, CipherType };
|
||||
|
||||
@@ -667,3 +669,31 @@ export function constantTimeEqual(
|
||||
): boolean {
|
||||
return crypto.constantTimeEqual(left, right);
|
||||
}
|
||||
|
||||
export function generateAvatarColor({
|
||||
aci,
|
||||
e164,
|
||||
pni,
|
||||
groupId,
|
||||
}: {
|
||||
aci: AciString | undefined;
|
||||
e164: string | undefined;
|
||||
pni: PniString | undefined;
|
||||
groupId: string | undefined;
|
||||
}): string {
|
||||
let identifier: Uint8Array;
|
||||
if (aci != null) {
|
||||
identifier = Aci.parseFromServiceIdString(aci).getServiceIdBinary();
|
||||
} else if (e164 != null) {
|
||||
identifier = Bytes.fromString(e164);
|
||||
} else if (pni != null) {
|
||||
identifier = Pni.parseFromServiceIdString(pni).getServiceIdBinary();
|
||||
} else if (groupId != null) {
|
||||
identifier = Bytes.fromBase64(groupId);
|
||||
} else {
|
||||
return sample(AvatarColors) || AvatarColors[0];
|
||||
}
|
||||
|
||||
const digest = hash(HashType.size256, identifier);
|
||||
return AvatarColors[digest[0] % AVATAR_COLOR_COUNT];
|
||||
}
|
||||
|
3
ts/model-types.d.ts
vendored
3
ts/model-types.d.ts
vendored
@@ -345,6 +345,9 @@ export type ConversationAttributesType = {
|
||||
>;
|
||||
capabilities?: CapabilitiesType;
|
||||
color?: string;
|
||||
// If present - the numeric value of `color` (possibly not yet supported) that
|
||||
// we got the from primary during either backup or storage service import.
|
||||
colorFromPrimary?: number;
|
||||
conversationColor?: ConversationColorType;
|
||||
customColor?: CustomColorType;
|
||||
customColorId?: string;
|
||||
|
@@ -5273,7 +5273,12 @@ export class ConversationModel extends window.Backbone
|
||||
}
|
||||
|
||||
getColor(): AvatarColorType {
|
||||
return migrateColor(this.getServiceId(), this.get('color'));
|
||||
return migrateColor(this.get('color'), {
|
||||
aci: this.getAci(),
|
||||
e164: this.get('e164'),
|
||||
pni: this.getPni(),
|
||||
groupId: this.get('groupId'),
|
||||
});
|
||||
}
|
||||
|
||||
getConversationColor(): ConversationColorType | undefined {
|
||||
|
@@ -708,6 +708,7 @@ export class BackupExportStream extends Readable {
|
||||
),
|
||||
}
|
||||
: null,
|
||||
svrPin: storage.get('svrPin'),
|
||||
accountSettings: {
|
||||
readReceipts: storage.get('read-receipt-setting'),
|
||||
sealedSenderIndicators: storage.get('sealedSenderIndicators'),
|
||||
@@ -824,7 +825,9 @@ export class BackupExportStream extends Readable {
|
||||
};
|
||||
|
||||
if (isMe(convo)) {
|
||||
res.self = {};
|
||||
res.self = {
|
||||
avatarColor: toAvatarColor(convo.color),
|
||||
};
|
||||
} else if (isDirectConversation(convo)) {
|
||||
let visibility: Backups.Contact.Visibility;
|
||||
if (convo.removalStage == null) {
|
||||
@@ -880,6 +883,7 @@ export class BackupExportStream extends Readable {
|
||||
systemNickname: convo.systemNickname,
|
||||
hideStory: convo.hideStory === true,
|
||||
identityKey: identityKey?.publicKey || null,
|
||||
avatarColor: toAvatarColor(convo.color),
|
||||
|
||||
// Integer values match so we can use it as is
|
||||
identityState: identityKey?.verified ?? 0,
|
||||
@@ -916,6 +920,7 @@ export class BackupExportStream extends Readable {
|
||||
blocked: convo.groupId
|
||||
? window.storage.blocked.isGroupBlocked(convo.groupId)
|
||||
: false,
|
||||
avatarColor: toAvatarColor(convo.color),
|
||||
snapshot: {
|
||||
title: {
|
||||
title: convo.name?.trim() ?? '',
|
||||
@@ -3056,3 +3061,36 @@ function toCallLinkRestrictionsProto(
|
||||
|
||||
return values.UNKNOWN;
|
||||
}
|
||||
|
||||
function toAvatarColor(
|
||||
color: string | undefined
|
||||
): Backups.AvatarColor | undefined {
|
||||
switch (color) {
|
||||
case 'A100':
|
||||
return Backups.AvatarColor.A100;
|
||||
case 'A110':
|
||||
return Backups.AvatarColor.A110;
|
||||
case 'A120':
|
||||
return Backups.AvatarColor.A120;
|
||||
case 'A130':
|
||||
return Backups.AvatarColor.A130;
|
||||
case 'A140':
|
||||
return Backups.AvatarColor.A140;
|
||||
case 'A150':
|
||||
return Backups.AvatarColor.A150;
|
||||
case 'A160':
|
||||
return Backups.AvatarColor.A160;
|
||||
case 'A170':
|
||||
return Backups.AvatarColor.A170;
|
||||
case 'A180':
|
||||
return Backups.AvatarColor.A180;
|
||||
case 'A190':
|
||||
return Backups.AvatarColor.A190;
|
||||
case 'A200':
|
||||
return Backups.AvatarColor.A200;
|
||||
case 'A210':
|
||||
return Backups.AvatarColor.A210;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -465,8 +465,7 @@ export class BackupImportStream extends Writable {
|
||||
// Not yet supported
|
||||
return;
|
||||
} else if (recipient.self) {
|
||||
strictAssert(this.#ourConversation != null, 'Missing account data');
|
||||
convo = this.#ourConversation;
|
||||
convo = this.#fromSelf(recipient.self);
|
||||
} else if (recipient.group) {
|
||||
convo = await this.#fromGroup(recipient.group);
|
||||
} else if (recipient.distributionList) {
|
||||
@@ -684,6 +683,7 @@ export class BackupImportStream extends Writable {
|
||||
backupsSubscriberData,
|
||||
donationSubscriberData,
|
||||
accountSettings,
|
||||
svrPin,
|
||||
}: Backups.IAccountData): Promise<void> {
|
||||
strictAssert(this.#ourConversation === undefined, 'Duplicate AccountData');
|
||||
const me =
|
||||
@@ -805,6 +805,9 @@ export class BackupImportStream extends Writable {
|
||||
'preferredReactionEmoji',
|
||||
accountSettings?.preferredReactionEmoji || []
|
||||
);
|
||||
if (svrPin) {
|
||||
await storage.put('svrPin', svrPin);
|
||||
}
|
||||
|
||||
const { PhoneNumberSharingMode: BackupMode } = Backups.AccountData;
|
||||
switch (accountSettings?.phoneNumberSharingMode) {
|
||||
@@ -879,6 +882,18 @@ export class BackupImportStream extends Writable {
|
||||
await this.#updateConversation(me);
|
||||
}
|
||||
|
||||
#fromSelf(self: Backups.ISelf): ConversationAttributesType {
|
||||
strictAssert(this.#ourConversation != null, 'Missing account data');
|
||||
const convo = this.#ourConversation;
|
||||
|
||||
if (self.avatarColor != null) {
|
||||
convo.color = fromAvatarColor(self.avatarColor);
|
||||
convo.colorFromPrimary = dropNull(self.avatarColor);
|
||||
}
|
||||
|
||||
return convo;
|
||||
}
|
||||
|
||||
async #fromContact(
|
||||
contact: Backups.IContact
|
||||
): Promise<ConversationAttributesType> {
|
||||
@@ -934,6 +949,8 @@ export class BackupImportStream extends Writable {
|
||||
nicknameGivenName: dropNull(contact.nickname?.given),
|
||||
nicknameFamilyName: dropNull(contact.nickname?.family),
|
||||
note: dropNull(contact.note),
|
||||
color: fromAvatarColor(contact.avatarColor),
|
||||
colorFromPrimary: dropNull(contact.avatarColor),
|
||||
};
|
||||
|
||||
if (serviceId != null && Bytes.isNotEmpty(contact.identityKey)) {
|
||||
@@ -1035,6 +1052,8 @@ export class BackupImportStream extends Writable {
|
||||
url: avatarUrl,
|
||||
}
|
||||
: undefined,
|
||||
color: fromAvatarColor(group.avatarColor),
|
||||
colorFromPrimary: dropNull(group.avatarColor),
|
||||
|
||||
// Snapshot
|
||||
name: dropNull(title?.title)?.trim(),
|
||||
@@ -3744,3 +3763,39 @@ function fromCallLinkRestrictionsProto(
|
||||
|
||||
return CallLinkRestrictions.Unknown;
|
||||
}
|
||||
|
||||
function fromAvatarColor(
|
||||
color: Backups.AvatarColor | null | undefined
|
||||
): string | undefined {
|
||||
switch (color) {
|
||||
case Backups.AvatarColor.A100:
|
||||
return 'A100';
|
||||
case Backups.AvatarColor.A110:
|
||||
return 'A110';
|
||||
case Backups.AvatarColor.A120:
|
||||
return 'A120';
|
||||
case Backups.AvatarColor.A130:
|
||||
return 'A130';
|
||||
case Backups.AvatarColor.A140:
|
||||
return 'A140';
|
||||
case Backups.AvatarColor.A150:
|
||||
return 'A150';
|
||||
case Backups.AvatarColor.A160:
|
||||
return 'A160';
|
||||
case Backups.AvatarColor.A170:
|
||||
return 'A170';
|
||||
case Backups.AvatarColor.A180:
|
||||
return 'A180';
|
||||
case Backups.AvatarColor.A190:
|
||||
return 'A190';
|
||||
case Backups.AvatarColor.A200:
|
||||
return 'A200';
|
||||
case Backups.AvatarColor.A210:
|
||||
return 'A210';
|
||||
case null:
|
||||
case undefined:
|
||||
return undefined;
|
||||
default:
|
||||
throw missingCaseError(color);
|
||||
}
|
||||
}
|
||||
|
@@ -139,6 +139,52 @@ function fromRecordVerified(
|
||||
}
|
||||
}
|
||||
|
||||
function fromAvatarColor(
|
||||
color: Proto.AvatarColor | null | undefined
|
||||
): string | undefined {
|
||||
switch (color) {
|
||||
case Proto.AvatarColor.A100:
|
||||
return 'A100';
|
||||
case Proto.AvatarColor.A110:
|
||||
return 'A110';
|
||||
case Proto.AvatarColor.A120:
|
||||
return 'A120';
|
||||
case Proto.AvatarColor.A130:
|
||||
return 'A130';
|
||||
case Proto.AvatarColor.A140:
|
||||
return 'A140';
|
||||
case Proto.AvatarColor.A150:
|
||||
return 'A150';
|
||||
case Proto.AvatarColor.A160:
|
||||
return 'A160';
|
||||
case Proto.AvatarColor.A170:
|
||||
return 'A170';
|
||||
case Proto.AvatarColor.A180:
|
||||
return 'A180';
|
||||
case Proto.AvatarColor.A190:
|
||||
return 'A190';
|
||||
case Proto.AvatarColor.A200:
|
||||
return 'A200';
|
||||
case Proto.AvatarColor.A210:
|
||||
return 'A210';
|
||||
case undefined:
|
||||
case null:
|
||||
return undefined;
|
||||
default:
|
||||
throw missingCaseError(color);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAvatarColor(
|
||||
conversation: ConversationModel,
|
||||
protoColor: Proto.AvatarColor | null | undefined
|
||||
): void {
|
||||
conversation.set({
|
||||
colorFromPrimary: dropNull(protoColor),
|
||||
color: fromAvatarColor(protoColor) ?? conversation.get('color'),
|
||||
});
|
||||
}
|
||||
|
||||
function addUnknownFields(
|
||||
record: RecordClass,
|
||||
conversation: ConversationModel,
|
||||
@@ -260,6 +306,10 @@ export async function toContactRecord(
|
||||
contactRecord.unregisteredAtTimestamp = getSafeLongFromTimestamp(
|
||||
conversation.get('firstUnregisteredAt')
|
||||
);
|
||||
const avatarColor = conversation.get('colorFromPrimary');
|
||||
if (avatarColor != null) {
|
||||
contactRecord.avatarColor = avatarColor;
|
||||
}
|
||||
|
||||
applyUnknownFields(contactRecord, conversation);
|
||||
|
||||
@@ -485,6 +535,11 @@ export function toAccountRecord(
|
||||
}
|
||||
}
|
||||
|
||||
const avatarColor = conversation.get('colorFromPrimary');
|
||||
if (avatarColor != null) {
|
||||
accountRecord.avatarColor = avatarColor;
|
||||
}
|
||||
|
||||
applyUnknownFields(accountRecord, conversation);
|
||||
|
||||
return accountRecord;
|
||||
@@ -544,6 +599,11 @@ export function toGroupV2Record(
|
||||
}
|
||||
}
|
||||
|
||||
const avatarColor = conversation.get('colorFromPrimary');
|
||||
if (avatarColor != null) {
|
||||
groupV2Record.avatarColor = avatarColor;
|
||||
}
|
||||
|
||||
applyUnknownFields(groupV2Record, conversation);
|
||||
|
||||
return groupV2Record;
|
||||
@@ -1034,6 +1094,8 @@ export async function mergeGroupV2Record(
|
||||
|
||||
applyMessageRequestState(groupV2Record, conversation);
|
||||
|
||||
applyAvatarColor(conversation, groupV2Record.avatarColor);
|
||||
|
||||
let details = new Array<string>();
|
||||
|
||||
addUnknownFields(groupV2Record, conversation, details);
|
||||
@@ -1292,6 +1354,8 @@ export async function mergeContactRecord(
|
||||
});
|
||||
}
|
||||
|
||||
applyAvatarColor(conversation, contactRecord.avatarColor);
|
||||
|
||||
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
|
||||
await toContactRecord(conversation),
|
||||
contactRecord,
|
||||
@@ -1683,6 +1747,8 @@ export async function mergeAccountRecord(
|
||||
await window.storage.put('avatarUrl', avatarUrl);
|
||||
}
|
||||
|
||||
applyAvatarColor(conversation, accountRecord.avatarColor);
|
||||
|
||||
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
|
||||
toAccountRecord(conversation),
|
||||
accountRecord,
|
||||
|
@@ -19,6 +19,7 @@ import {
|
||||
decryptProfile,
|
||||
getRandomBytes,
|
||||
constantTimeEqual,
|
||||
generateAvatarColor,
|
||||
generateRegistrationId,
|
||||
deriveSecrets,
|
||||
encryptDeviceName,
|
||||
@@ -49,6 +50,7 @@ import {
|
||||
generateAttachmentKeys,
|
||||
type DecryptedAttachmentV2,
|
||||
} from '../AttachmentCrypto';
|
||||
import type { AciString, PniString } from '../types/ServiceId';
|
||||
import { createTempDir, deleteTempDir } from '../updater/common';
|
||||
import { uuidToBytes, bytesToUuid } from '../util/uuidToBytes';
|
||||
|
||||
@@ -1163,4 +1165,59 @@ describe('Crypto', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateAvatarColor', () => {
|
||||
const aci = 'a025bf78-653e-44e0-beb9-deb14ba32487' as AciString;
|
||||
const pni = 'PNI:11a175e3-fe31-4eda-87da-e0bf2a2e250b' as PniString;
|
||||
const e164 = '+12135550124';
|
||||
const groupId = 'BwJRIdomqOSOckHjnJsknNCibCZKJFt+RxLIpa9CWJ4=';
|
||||
|
||||
it('generates color based on ACI', () => {
|
||||
assert.strictEqual(
|
||||
generateAvatarColor({
|
||||
aci,
|
||||
e164,
|
||||
pni,
|
||||
groupId,
|
||||
}),
|
||||
'A140'
|
||||
);
|
||||
});
|
||||
|
||||
it('generates color based on E164', () => {
|
||||
assert.strictEqual(
|
||||
generateAvatarColor({
|
||||
aci: undefined,
|
||||
e164,
|
||||
pni,
|
||||
groupId,
|
||||
}),
|
||||
'A150'
|
||||
);
|
||||
});
|
||||
|
||||
it('generates color based on PNI', () => {
|
||||
assert.strictEqual(
|
||||
generateAvatarColor({
|
||||
aci: undefined,
|
||||
e164: undefined,
|
||||
pni,
|
||||
groupId,
|
||||
}),
|
||||
'A200'
|
||||
);
|
||||
});
|
||||
|
||||
it('generates color based on group id', () => {
|
||||
assert.strictEqual(
|
||||
generateAvatarColor({
|
||||
aci: undefined,
|
||||
e164: undefined,
|
||||
pni: undefined,
|
||||
groupId,
|
||||
}),
|
||||
'A130'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -39,7 +39,7 @@ import type {
|
||||
ChatServerMessageAck,
|
||||
ChatServiceListener,
|
||||
ConnectionEventsListener,
|
||||
} from '@signalapp/libsignal-client/dist/net';
|
||||
} from '@signalapp/libsignal-client/dist/net/Chat';
|
||||
import type { EventHandler } from './EventTarget';
|
||||
import EventTarget from './EventTarget';
|
||||
|
||||
|
3
ts/types/Storage.d.ts
vendored
3
ts/types/Storage.d.ts
vendored
@@ -225,6 +225,9 @@ export type StorageAccessType = {
|
||||
// The `firstAppVersion` present on an BackupInfo from an imported backup.
|
||||
restoredBackupFirstAppVersion: string;
|
||||
|
||||
// Stored solely for pesistance during import/export sequence
|
||||
svrPin: string;
|
||||
|
||||
postRegistrationSyncsStatus: 'incomplete' | 'complete';
|
||||
|
||||
// Deprecated
|
||||
|
@@ -36,6 +36,7 @@ import {
|
||||
canHaveUsername,
|
||||
} from './getTitle';
|
||||
import { hasDraft } from './hasDraft';
|
||||
import { isAciString } from './isAciString';
|
||||
import { isBlocked } from './isBlocked';
|
||||
import { isConversationAccepted } from './isConversationAccepted';
|
||||
import {
|
||||
@@ -89,7 +90,12 @@ export function getConversation(model: ConversationModel): ConversationType {
|
||||
const ourAci = window.textsecure.storage.user.getAci();
|
||||
const ourPni = window.textsecure.storage.user.getPni();
|
||||
|
||||
const color = migrateColor(attributes.serviceId, attributes.color);
|
||||
const color = migrateColor(attributes.color, {
|
||||
aci: isAciString(attributes.serviceId) ? attributes.serviceId : undefined,
|
||||
e164: attributes.e164,
|
||||
pni: attributes.pni,
|
||||
groupId: attributes.groupId,
|
||||
});
|
||||
|
||||
const { draftTimestamp, draftEditMessage, timestamp } = attributes;
|
||||
const draftPreview = getDraftPreview(attributes);
|
||||
|
@@ -1,29 +1,22 @@
|
||||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { sample } from 'lodash';
|
||||
|
||||
import { AVATAR_COLOR_COUNT, AvatarColors } from '../types/Colors';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import type { ConversationAttributesType } from '../model-types';
|
||||
import type { AvatarColorType, CustomColorType } from '../types/Colors';
|
||||
import type { ServiceIdString } from '../types/ServiceId';
|
||||
import { generateAvatarColor } from '../Crypto';
|
||||
|
||||
const NEW_COLOR_NAMES = new Set(AvatarColors);
|
||||
|
||||
export function migrateColor(
|
||||
serviceId?: ServiceIdString,
|
||||
color?: string
|
||||
color: string | undefined,
|
||||
options: Parameters<typeof generateAvatarColor>[0]
|
||||
): AvatarColorType {
|
||||
if (color && NEW_COLOR_NAMES.has(color)) {
|
||||
return color;
|
||||
}
|
||||
|
||||
if (!serviceId) {
|
||||
return sample(AvatarColors) || AvatarColors[0];
|
||||
}
|
||||
|
||||
const index = (parseInt(serviceId.slice(-4), 16) || 0) % AVATAR_COLOR_COUNT;
|
||||
return AvatarColors[index];
|
||||
return generateAvatarColor(options);
|
||||
}
|
||||
|
||||
export function getCustomColorData(conversation: ConversationAttributesType): {
|
||||
|
Reference in New Issue
Block a user