Import/export chat styles
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
parent
97c52662e0
commit
6e9b690068
15 changed files with 663 additions and 47 deletions
|
@ -78,6 +78,7 @@ message AccountData {
|
|||
bool hasCompletedUsernameOnboarding = 16;
|
||||
PhoneNumberSharingMode phoneNumberSharingMode = 17;
|
||||
ChatStyle defaultChatStyle = 18;
|
||||
repeated ChatStyle.CustomChatColor customChatColors = 19;
|
||||
}
|
||||
|
||||
message SubscriberData {
|
||||
|
@ -1055,6 +1056,15 @@ message ChatStyle {
|
|||
repeated float positions = 3; // percent from 0 to 1
|
||||
}
|
||||
|
||||
message CustomChatColor {
|
||||
uint32 id = 1;
|
||||
|
||||
oneof color {
|
||||
uint32 solid = 2;
|
||||
Gradient gradient = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message AutomaticBubbleColor {
|
||||
}
|
||||
|
||||
|
@ -1115,10 +1125,12 @@ message ChatStyle {
|
|||
}
|
||||
|
||||
oneof bubbleColor {
|
||||
BubbleColorPreset bubbleColorPreset = 3;
|
||||
Gradient bubbleGradient = 4;
|
||||
uint32 bubbleSolidColor = 5;
|
||||
// Bubble setting is automatically determined based on the wallpaper setting.
|
||||
AutomaticBubbleColor autoBubbleColor = 6;
|
||||
// Bubble setting is automatically determined based on the wallpaper setting,
|
||||
// or `SOLID_ULTRAMARINE` for `noWallpaper`
|
||||
AutomaticBubbleColor autoBubbleColor = 3;
|
||||
BubbleColorPreset bubbleColorPreset = 4;
|
||||
|
||||
// See AccountSettings.customChatColors
|
||||
uint32 customColorId = 5;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { hslToRGB } from '../../util/hslToRGB';
|
||||
|
||||
function getRatio(min: number, max: number, value: number) {
|
||||
return (value - min) / (max - min);
|
||||
}
|
||||
|
@ -22,30 +24,6 @@ function getHSLValues(percentage: number): [number, number, number] {
|
|||
return [338 * ratio, 1, 0.5];
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative
|
||||
function hslToRGB(
|
||||
h: number,
|
||||
s: number,
|
||||
l: number
|
||||
): {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
} {
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
|
||||
function f(n: number): number {
|
||||
const k = (n + h / 30) % 12;
|
||||
return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
}
|
||||
|
||||
return {
|
||||
r: Math.round(255 * f(0)),
|
||||
g: Math.round(255 * f(8)),
|
||||
b: Math.round(255 * f(4)),
|
||||
};
|
||||
}
|
||||
|
||||
export function getHSL(percentage: number): string {
|
||||
const [h, s, l] = getHSLValues(percentage);
|
||||
return `hsl(${h}, ${s * 100}%, ${l * 100}%)`;
|
||||
|
|
5
ts/model-types.d.ts
vendored
5
ts/model-types.d.ts
vendored
|
@ -329,6 +329,11 @@ export type ConversationAttributesType = {
|
|||
conversationColor?: ConversationColorType;
|
||||
customColor?: CustomColorType;
|
||||
customColorId?: string;
|
||||
|
||||
// Set at backup import time, exported as is.
|
||||
wallpaperPhotoPointerBase64?: string;
|
||||
wallpaperPreset?: number;
|
||||
|
||||
discoveredUnregisteredAt?: number;
|
||||
firstUnregisteredAt?: number;
|
||||
draftChanged?: boolean;
|
||||
|
|
|
@ -1,4 +1,37 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Backups } from '../../protobuf';
|
||||
import type { ConversationColorType } from '../../types/Colors';
|
||||
|
||||
export const BACKUP_VERSION = 1;
|
||||
|
||||
const { WallpaperPreset } = Backups.ChatStyle;
|
||||
|
||||
// See https://github.com/signalapp/Signal-Android-Private/blob/4a41e9f9a1ed0aba7cae0e0dc4dbcac50fddc469/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ChatColorsMapper.kt#L32
|
||||
export const WALLPAPER_TO_BUBBLE_COLOR = new Map<
|
||||
Backups.ChatStyle.WallpaperPreset,
|
||||
ConversationColorType
|
||||
>([
|
||||
[WallpaperPreset.SOLID_BLUSH, 'crimson'],
|
||||
[WallpaperPreset.SOLID_COPPER, 'vermilion'],
|
||||
[WallpaperPreset.SOLID_DUST, 'burlap'],
|
||||
[WallpaperPreset.SOLID_CELADON, 'forest'],
|
||||
[WallpaperPreset.SOLID_RAINFOREST, 'wintergreen'],
|
||||
[WallpaperPreset.SOLID_PACIFIC, 'teal'],
|
||||
[WallpaperPreset.SOLID_FROST, 'blue'],
|
||||
[WallpaperPreset.SOLID_NAVY, 'indigo'],
|
||||
[WallpaperPreset.SOLID_LILAC, 'violet'],
|
||||
[WallpaperPreset.SOLID_PINK, 'plum'],
|
||||
[WallpaperPreset.SOLID_EGGPLANT, 'taupe'],
|
||||
[WallpaperPreset.SOLID_SILVER, 'steel'],
|
||||
[WallpaperPreset.GRADIENT_SUNSET, 'ember'],
|
||||
[WallpaperPreset.GRADIENT_NOIR, 'midnight'],
|
||||
[WallpaperPreset.GRADIENT_HEATMAP, 'infrared'],
|
||||
[WallpaperPreset.GRADIENT_AQUA, 'lagoon'],
|
||||
[WallpaperPreset.GRADIENT_IRIDESCENT, 'fluorescent'],
|
||||
[WallpaperPreset.GRADIENT_MONSTERA, 'basil'],
|
||||
[WallpaperPreset.GRADIENT_BLISS, 'sublime'],
|
||||
[WallpaperPreset.GRADIENT_SKY, 'sea'],
|
||||
[WallpaperPreset.GRADIENT_PEACH, 'tangerine'],
|
||||
]);
|
||||
|
|
|
@ -84,7 +84,8 @@ import { getCallsHistoryForRedux } from '../callHistoryLoader';
|
|||
import { makeLookup } from '../../util/makeLookup';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import { isAciString } from '../../util/isAciString';
|
||||
import type { AboutMe } from './types';
|
||||
import { hslToRGB } from '../../util/hslToRGB';
|
||||
import type { AboutMe, LocalChatStyle } from './types';
|
||||
import { messageHasPaymentEvent } from '../../messages/helpers';
|
||||
import {
|
||||
numberToAddressType,
|
||||
|
@ -173,6 +174,10 @@ export class BackupExportStream extends Readable {
|
|||
private nextRecipientId = 0;
|
||||
private flushResolve: (() => void) | undefined;
|
||||
|
||||
// Map from custom color uuid to an index in accountSettings.customColors
|
||||
// array.
|
||||
private customColorIdByUuid = new Map<string, number>();
|
||||
|
||||
public run(backupLevel: BackupLevel): void {
|
||||
drop(
|
||||
(async () => {
|
||||
|
@ -338,6 +343,15 @@ export class BackupExportStream extends Readable {
|
|||
markedUnread: attributes.markedUnread === true,
|
||||
dontNotifyForMentionsIfMuted:
|
||||
attributes.dontNotifyForMentionsIfMuted === true,
|
||||
|
||||
style: this.toChatStyle({
|
||||
wallpaperPhotoPointer: attributes.wallpaperPhotoPointerBase64
|
||||
? Bytes.fromBase64(attributes.wallpaperPhotoPointerBase64)
|
||||
: undefined,
|
||||
wallpaperPreset: attributes.wallpaperPreset,
|
||||
color: attributes.conversationColor,
|
||||
customColorId: attributes.customColorId,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -545,6 +559,10 @@ export class BackupExportStream extends Readable {
|
|||
'hasSeenGroupStoryEducationSheet'
|
||||
),
|
||||
phoneNumberSharingMode,
|
||||
// Note that this should be called before `toDefaultChatStyle` because
|
||||
// it builds `customColorIdByUuid`
|
||||
customChatColors: this.toCustomChatColors(),
|
||||
defaultChatStyle: this.toDefaultChatStyle(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -2237,6 +2255,160 @@ export class BackupExportStream extends Readable {
|
|||
.reverse()
|
||||
);
|
||||
}
|
||||
|
||||
private toCustomChatColors(): Array<Backups.ChatStyle.ICustomChatColor> {
|
||||
const customColors = window.storage.get('customColors');
|
||||
if (!customColors) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = new Array<Backups.ChatStyle.ICustomChatColor>();
|
||||
for (const [uuid, color] of Object.entries(customColors.colors)) {
|
||||
const id = result.length;
|
||||
this.customColorIdByUuid.set(uuid, id);
|
||||
|
||||
const start = hslToRGBInt(color.start.hue, color.start.saturation);
|
||||
|
||||
if (color.end == null) {
|
||||
result.push({
|
||||
id,
|
||||
solid: start,
|
||||
});
|
||||
} else {
|
||||
const end = hslToRGBInt(color.end.hue, color.end.saturation);
|
||||
|
||||
result.push({
|
||||
id,
|
||||
gradient: {
|
||||
colors: [start, end],
|
||||
positions: [0, 1],
|
||||
angle: color.deg,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private toDefaultChatStyle(): Backups.IChatStyle {
|
||||
const defaultColor = window.storage.get('defaultConversationColor');
|
||||
|
||||
return this.toChatStyle({
|
||||
wallpaperPhotoPointer: window.storage.get('defaultWallpaperPhotoPointer'),
|
||||
wallpaperPreset: window.storage.get('defaultWallpaperPreset'),
|
||||
color: defaultColor?.color,
|
||||
customColorId: defaultColor?.customColorData?.id,
|
||||
});
|
||||
}
|
||||
|
||||
private toChatStyle({
|
||||
wallpaperPhotoPointer,
|
||||
wallpaperPreset,
|
||||
color,
|
||||
customColorId,
|
||||
}: LocalChatStyle): Backups.IChatStyle {
|
||||
const result: Backups.IChatStyle = {};
|
||||
|
||||
if (Bytes.isNotEmpty(wallpaperPhotoPointer)) {
|
||||
result.wallpaperPhoto = Backups.FilePointer.decode(wallpaperPhotoPointer);
|
||||
} else if (wallpaperPreset) {
|
||||
result.wallpaperPreset = wallpaperPreset;
|
||||
}
|
||||
|
||||
if (color == null) {
|
||||
result.autoBubbleColor = {};
|
||||
return result;
|
||||
}
|
||||
|
||||
if (color === 'custom') {
|
||||
strictAssert(
|
||||
customColorId != null,
|
||||
'No custom color id for custom color'
|
||||
);
|
||||
|
||||
const index = this.customColorIdByUuid.get(customColorId);
|
||||
strictAssert(index != null, 'Missing custom color');
|
||||
|
||||
result.customColorId = index;
|
||||
return result;
|
||||
}
|
||||
|
||||
const { BubbleColorPreset } = Backups.ChatStyle;
|
||||
|
||||
switch (color) {
|
||||
case 'ultramarine':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_ULTRAMARINE;
|
||||
break;
|
||||
case 'crimson':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_CRIMSON;
|
||||
break;
|
||||
case 'vermilion':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_VERMILION;
|
||||
break;
|
||||
case 'burlap':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_BURLAP;
|
||||
break;
|
||||
case 'forest':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_FOREST;
|
||||
break;
|
||||
case 'wintergreen':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_WINTERGREEN;
|
||||
break;
|
||||
case 'teal':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_TEAL;
|
||||
break;
|
||||
case 'blue':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_BLUE;
|
||||
break;
|
||||
case 'indigo':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_INDIGO;
|
||||
break;
|
||||
case 'violet':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_VIOLET;
|
||||
break;
|
||||
case 'plum':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_PLUM;
|
||||
break;
|
||||
case 'taupe':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_TAUPE;
|
||||
break;
|
||||
case 'steel':
|
||||
result.bubbleColorPreset = BubbleColorPreset.SOLID_STEEL;
|
||||
break;
|
||||
case 'ember':
|
||||
result.bubbleColorPreset = BubbleColorPreset.GRADIENT_EMBER;
|
||||
break;
|
||||
case 'midnight':
|
||||
result.bubbleColorPreset = BubbleColorPreset.GRADIENT_MIDNIGHT;
|
||||
break;
|
||||
case 'infrared':
|
||||
result.bubbleColorPreset = BubbleColorPreset.GRADIENT_INFRARED;
|
||||
break;
|
||||
case 'lagoon':
|
||||
result.bubbleColorPreset = BubbleColorPreset.GRADIENT_LAGOON;
|
||||
break;
|
||||
case 'fluorescent':
|
||||
result.bubbleColorPreset = BubbleColorPreset.GRADIENT_FLUORESCENT;
|
||||
break;
|
||||
case 'basil':
|
||||
result.bubbleColorPreset = BubbleColorPreset.GRADIENT_BASIL;
|
||||
break;
|
||||
case 'sublime':
|
||||
result.bubbleColorPreset = BubbleColorPreset.GRADIENT_SUBLIME;
|
||||
break;
|
||||
case 'sea':
|
||||
result.bubbleColorPreset = BubbleColorPreset.GRADIENT_SEA;
|
||||
break;
|
||||
case 'tangerine':
|
||||
result.bubbleColorPreset = BubbleColorPreset.GRADIENT_TANGERINE;
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(color);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function checkServiceIdEquivalence(
|
||||
|
@ -2248,3 +2420,9 @@ function checkServiceIdEquivalence(
|
|||
|
||||
return leftConvo && rightConvo && leftConvo === rightConvo;
|
||||
}
|
||||
|
||||
function hslToRGBInt(hue: number, saturation: number): number {
|
||||
const { r, g, b } = hslToRGB(hue, saturation, 1);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return ((0xff << 24) | (r << 16) | (g << 8) | b) >>> 0;
|
||||
}
|
||||
|
|
|
@ -32,6 +32,12 @@ import {
|
|||
STICKERPACK_KEY_BYTE_LEN,
|
||||
downloadStickerPack,
|
||||
} from '../../types/Stickers';
|
||||
import type {
|
||||
ConversationColorType,
|
||||
CustomColorsItemType,
|
||||
CustomColorType,
|
||||
CustomColorDataType,
|
||||
} from '../../types/Colors';
|
||||
import type {
|
||||
ConversationAttributesType,
|
||||
CustomError,
|
||||
|
@ -61,13 +67,14 @@ import { SendStatus } from '../../messages/MessageSendState';
|
|||
import type { SendStateByConversationId } from '../../messages/MessageSendState';
|
||||
import { SeenStatus } from '../../MessageSeenStatus';
|
||||
import * as Bytes from '../../Bytes';
|
||||
import { BACKUP_VERSION } from './constants';
|
||||
import type { AboutMe } from './types';
|
||||
import { BACKUP_VERSION, WALLPAPER_TO_BUBBLE_COLOR } from './constants';
|
||||
import type { AboutMe, LocalChatStyle } from './types';
|
||||
import type { GroupV2ChangeDetailType } from '../../groups';
|
||||
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
|
||||
import { drop } from '../../util/drop';
|
||||
import { isNotNil } from '../../util/isNotNil';
|
||||
import { isGroup, isGroupV2 } from '../../util/whatTypeOfConversation';
|
||||
import { rgbToHSL } from '../../util/rgbToHSL';
|
||||
import {
|
||||
convertBackupMessageAttachmentToAttachment,
|
||||
convertFilePointerToAttachment,
|
||||
|
@ -249,6 +256,7 @@ export class BackupImportStream extends Writable {
|
|||
});
|
||||
private ourConversation?: ConversationAttributesType;
|
||||
private pinnedConversations = new Array<[number, string]>();
|
||||
private customColorById = new Map<number, CustomColorDataType>();
|
||||
|
||||
constructor() {
|
||||
super({ objectMode: true });
|
||||
|
@ -613,6 +621,34 @@ export class BackupImportStream extends Writable {
|
|||
);
|
||||
}
|
||||
|
||||
// It is important to import custom chat colors before default styles
|
||||
// because we build the uuid => integer id map for the colors.
|
||||
await this.fromCustomChatColors(accountSettings?.customChatColors);
|
||||
|
||||
const defaultChatStyle = this.fromChatStyle(
|
||||
accountSettings?.defaultChatStyle
|
||||
);
|
||||
|
||||
if (defaultChatStyle.color != null) {
|
||||
await window.storage.put('defaultConversationColor', {
|
||||
color: defaultChatStyle.color,
|
||||
customColorData: defaultChatStyle.customColorData,
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultChatStyle.wallpaperPhotoPointer != null) {
|
||||
await window.storage.put(
|
||||
'defaultWallpaperPhotoPointer',
|
||||
defaultChatStyle.wallpaperPhotoPointer
|
||||
);
|
||||
}
|
||||
if (defaultChatStyle.wallpaperPreset != null) {
|
||||
await window.storage.put(
|
||||
'defaultWallpaperPreset',
|
||||
defaultChatStyle.wallpaperPreset
|
||||
);
|
||||
}
|
||||
|
||||
this.updateConversation(me);
|
||||
}
|
||||
|
||||
|
@ -959,6 +995,24 @@ export class BackupImportStream extends Writable {
|
|||
conversation.dontNotifyForMentionsIfMuted =
|
||||
chat.dontNotifyForMentionsIfMuted === true;
|
||||
|
||||
const chatStyle = this.fromChatStyle(chat.style);
|
||||
|
||||
if (chatStyle.wallpaperPhotoPointer != null) {
|
||||
conversation.wallpaperPhotoPointerBase64 = Bytes.toBase64(
|
||||
chatStyle.wallpaperPhotoPointer
|
||||
);
|
||||
}
|
||||
if (chatStyle.wallpaperPreset != null) {
|
||||
conversation.wallpaperPreset = chatStyle.wallpaperPreset;
|
||||
}
|
||||
if (chatStyle.color != null) {
|
||||
conversation.conversationColor = chatStyle.color;
|
||||
}
|
||||
if (chatStyle.customColorData != null) {
|
||||
conversation.customColor = chatStyle.customColorData.value;
|
||||
conversation.customColorId = chatStyle.customColorData.id;
|
||||
}
|
||||
|
||||
this.updateConversation(conversation);
|
||||
|
||||
if (chat.pinnedOrder != null) {
|
||||
|
@ -2477,4 +2531,188 @@ export class BackupImportStream extends Writable {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async fromCustomChatColors(
|
||||
customChatColors:
|
||||
| ReadonlyArray<Backups.ChatStyle.ICustomChatColor>
|
||||
| undefined
|
||||
| null
|
||||
): Promise<void> {
|
||||
if (!customChatColors?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customColors: CustomColorsItemType = {
|
||||
version: 1,
|
||||
colors: {},
|
||||
};
|
||||
|
||||
for (const color of customChatColors) {
|
||||
const uuid = generateUuid();
|
||||
let value: CustomColorType;
|
||||
|
||||
if (color.solid) {
|
||||
value = {
|
||||
start: rgbIntToHSL(color.solid),
|
||||
};
|
||||
} else {
|
||||
strictAssert(color.gradient != null, 'Either solid or gradient');
|
||||
strictAssert(color.gradient.colors != null, 'Missing gradient colors');
|
||||
|
||||
const start = color.gradient.colors.at(0);
|
||||
const end = color.gradient.colors.at(-1);
|
||||
const deg = color.gradient.angle;
|
||||
|
||||
strictAssert(start != null, 'Missing start color');
|
||||
strictAssert(end != null, 'Missing end color');
|
||||
strictAssert(deg != null, 'Missing angle');
|
||||
|
||||
value = {
|
||||
start: rgbIntToHSL(start),
|
||||
end: rgbIntToHSL(end),
|
||||
deg,
|
||||
};
|
||||
}
|
||||
|
||||
customColors.colors[uuid] = value;
|
||||
this.customColorById.set(color.id || 0, {
|
||||
id: uuid,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
await window.storage.put('customColors', customColors);
|
||||
}
|
||||
|
||||
private fromChatStyle(chatStyle: Backups.IChatStyle | null | undefined): Omit<
|
||||
LocalChatStyle,
|
||||
'customColorId'
|
||||
> & {
|
||||
customColorData: CustomColorDataType | undefined;
|
||||
} {
|
||||
if (!chatStyle) {
|
||||
return {
|
||||
wallpaperPhotoPointer: undefined,
|
||||
wallpaperPreset: undefined,
|
||||
color: 'ultramarine',
|
||||
customColorData: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
let wallpaperPhotoPointer: Uint8Array | undefined;
|
||||
let wallpaperPreset: number | undefined;
|
||||
|
||||
if (chatStyle.wallpaperPhoto) {
|
||||
wallpaperPhotoPointer = Backups.FilePointer.encode(
|
||||
chatStyle.wallpaperPhoto
|
||||
).finish();
|
||||
} else if (chatStyle.wallpaperPreset != null) {
|
||||
wallpaperPreset = chatStyle.wallpaperPreset;
|
||||
}
|
||||
|
||||
let color: ConversationColorType | undefined;
|
||||
let customColorData: CustomColorDataType | undefined;
|
||||
if (chatStyle.autoBubbleColor) {
|
||||
if (wallpaperPreset != null) {
|
||||
color = WALLPAPER_TO_BUBBLE_COLOR.get(wallpaperPreset) || 'ultramarine';
|
||||
} else {
|
||||
color = 'ultramarine';
|
||||
}
|
||||
} else if (chatStyle.bubbleColorPreset != null) {
|
||||
const { BubbleColorPreset } = Backups.ChatStyle;
|
||||
|
||||
switch (chatStyle.bubbleColorPreset) {
|
||||
case BubbleColorPreset.SOLID_CRIMSON:
|
||||
color = 'crimson';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_VERMILION:
|
||||
color = 'vermilion';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_BURLAP:
|
||||
color = 'burlap';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_FOREST:
|
||||
color = 'forest';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_WINTERGREEN:
|
||||
color = 'wintergreen';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_TEAL:
|
||||
color = 'teal';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_BLUE:
|
||||
color = 'blue';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_INDIGO:
|
||||
color = 'indigo';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_VIOLET:
|
||||
color = 'violet';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_PLUM:
|
||||
color = 'plum';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_TAUPE:
|
||||
color = 'taupe';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_STEEL:
|
||||
color = 'steel';
|
||||
break;
|
||||
case BubbleColorPreset.GRADIENT_EMBER:
|
||||
color = 'ember';
|
||||
break;
|
||||
case BubbleColorPreset.GRADIENT_MIDNIGHT:
|
||||
color = 'midnight';
|
||||
break;
|
||||
case BubbleColorPreset.GRADIENT_INFRARED:
|
||||
color = 'infrared';
|
||||
break;
|
||||
case BubbleColorPreset.GRADIENT_LAGOON:
|
||||
color = 'lagoon';
|
||||
break;
|
||||
case BubbleColorPreset.GRADIENT_FLUORESCENT:
|
||||
color = 'fluorescent';
|
||||
break;
|
||||
case BubbleColorPreset.GRADIENT_BASIL:
|
||||
color = 'basil';
|
||||
break;
|
||||
case BubbleColorPreset.GRADIENT_SUBLIME:
|
||||
color = 'sublime';
|
||||
break;
|
||||
case BubbleColorPreset.GRADIENT_SEA:
|
||||
color = 'sea';
|
||||
break;
|
||||
case BubbleColorPreset.GRADIENT_TANGERINE:
|
||||
color = 'tangerine';
|
||||
break;
|
||||
case BubbleColorPreset.SOLID_ULTRAMARINE:
|
||||
default:
|
||||
color = 'ultramarine';
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
strictAssert(chatStyle.customColorId != null, 'Missing custom color id');
|
||||
|
||||
const entry = this.customColorById.get(chatStyle.customColorId);
|
||||
strictAssert(entry != null, 'Missing custom color');
|
||||
|
||||
color = 'custom';
|
||||
customColorData = entry;
|
||||
}
|
||||
|
||||
return { wallpaperPhotoPointer, wallpaperPreset, color, customColorData };
|
||||
}
|
||||
}
|
||||
|
||||
function rgbIntToHSL(intValue: number): { hue: number; saturation: number } {
|
||||
const { h: hue, s: saturation } = rgbToHSL(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
(intValue >>> 16) & 0xff,
|
||||
// eslint-disable-next-line no-bitwise
|
||||
(intValue >>> 8) & 0xff,
|
||||
// eslint-disable-next-line no-bitwise
|
||||
intValue & 0xff
|
||||
);
|
||||
|
||||
return { hue, saturation };
|
||||
}
|
||||
|
|
8
ts/services/backups/types.d.ts
vendored
8
ts/services/backups/types.d.ts
vendored
|
@ -2,8 +2,16 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { AciString, PniString } from '../../types/ServiceId';
|
||||
import type { ConversationColorType } from '../../types/Colors';
|
||||
|
||||
export type AboutMe = {
|
||||
aci: AciString;
|
||||
pni?: PniString;
|
||||
};
|
||||
|
||||
export type LocalChatStyle = Readonly<{
|
||||
wallpaperPhotoPointer: Uint8Array | undefined;
|
||||
wallpaperPreset: number | undefined;
|
||||
color: ConversationColorType | undefined;
|
||||
customColorId: string | undefined;
|
||||
}>;
|
||||
|
|
|
@ -368,6 +368,7 @@ async function bulkAddSignedPreKeys(
|
|||
// Items
|
||||
|
||||
const ITEM_SPECS: Partial<Record<ItemKeyType, ObjectMappingSpecType>> = {
|
||||
defaultWallpaperPhotoPointer: ['value'],
|
||||
identityKeyMap: {
|
||||
key: 'value',
|
||||
valueSpec: {
|
||||
|
@ -379,6 +380,7 @@ const ITEM_SPECS: Partial<Record<ItemKeyType, ObjectMappingSpecType>> = {
|
|||
senderCertificate: ['value.serialized'],
|
||||
senderCertificateNoE164: ['value.serialized'],
|
||||
subscriberId: ['value'],
|
||||
backupsSubscriberId: ['value'],
|
||||
usernameLink: ['value.entropy', 'value.serverId'],
|
||||
};
|
||||
async function createOrUpdateItem<K extends ItemKeyType>(
|
||||
|
|
|
@ -162,7 +162,7 @@ describe('backup/bubble messages', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it.skip('roundtrips unopened gift badge', async () => {
|
||||
it('roundtrips unopened gift badge', async () => {
|
||||
await symmetricRoundtripHarness([
|
||||
{
|
||||
conversationId: contactA.id,
|
||||
|
@ -187,7 +187,7 @@ describe('backup/bubble messages', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it.skip('roundtrips opened gift badge', async () => {
|
||||
it('roundtrips opened gift badge', async () => {
|
||||
await symmetricRoundtripHarness([
|
||||
{
|
||||
conversationId: contactA.id,
|
||||
|
@ -212,7 +212,7 @@ describe('backup/bubble messages', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it.skip('roundtrips gift badge quote', async () => {
|
||||
it('roundtrips gift badge quote', async () => {
|
||||
await symmetricRoundtripHarness([
|
||||
{
|
||||
conversationId: contactA.id,
|
||||
|
|
|
@ -49,6 +49,7 @@ describe('backups', function (this: Mocha.Suite) {
|
|||
state = state.addContact(pinned, {
|
||||
identityKey: pinned.publicKey.serialize(),
|
||||
profileKey: pinned.profileKey.serialize(),
|
||||
whitelisted: true,
|
||||
});
|
||||
|
||||
state = state.pin(pinned);
|
||||
|
@ -99,19 +100,53 @@ describe('backups', function (this: Mocha.Suite) {
|
|||
const { contacts, phone, desktop, server } = bootstrap;
|
||||
const [friend, pinned] = contacts;
|
||||
|
||||
debug('wait for storage service sync to finish');
|
||||
{
|
||||
debug('wait for storage service sync to finish');
|
||||
const window = await app.getWindow();
|
||||
|
||||
const leftPane = window.locator('#LeftPane');
|
||||
await leftPane
|
||||
.locator(
|
||||
`[data-testid="${pinned.device.aci}"] >> "${pinned.profileName}"`
|
||||
)
|
||||
.waitFor();
|
||||
const contact = leftPane.locator(
|
||||
`[data-testid="${pinned.device.aci}"] >> "${pinned.profileName}"`
|
||||
);
|
||||
await contact.click();
|
||||
|
||||
debug('setting bubble color');
|
||||
const conversationStack = window.locator('.Inbox__conversation-stack');
|
||||
await conversationStack
|
||||
.locator('button.module-ConversationHeader__button--more')
|
||||
.click();
|
||||
|
||||
await window
|
||||
.locator('.react-contextmenu-item >> "Chat settings"')
|
||||
.click();
|
||||
|
||||
await conversationStack
|
||||
.locator('.ConversationDetails__chat-color')
|
||||
.click();
|
||||
await conversationStack
|
||||
.locator('.ChatColorPicker__bubble--infrared')
|
||||
.click();
|
||||
|
||||
const backButton = conversationStack.locator(
|
||||
'.ConversationPanel__header__back-button'
|
||||
);
|
||||
// Go back from colors
|
||||
await backButton.first().click();
|
||||
// Go back from settings
|
||||
await backButton.last().click();
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await server.send(
|
||||
desktop,
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await phone.encryptSyncSent(desktop, `to pinned ${i}`, {
|
||||
timestamp: bootstrap.getTimestamp(),
|
||||
destinationServiceId: pinned.device.aci,
|
||||
})
|
||||
);
|
||||
|
||||
const theirTimestamp = bootstrap.getTimestamp();
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
@ -159,14 +194,21 @@ describe('backups', function (this: Mocha.Suite) {
|
|||
app,
|
||||
async (window, snapshot) => {
|
||||
const leftPane = window.locator('#LeftPane');
|
||||
const pinnedElem = leftPane.locator(
|
||||
`[data-testid="${pinned.toContact().aci}"] >> "to pinned 4"`
|
||||
);
|
||||
|
||||
debug('Waiting for messages to pinned contact to come through');
|
||||
await pinnedElem.click();
|
||||
|
||||
const contactElem = leftPane.locator(
|
||||
`[data-testid="${friend.toContact().aci}"] >> "respond 4"`
|
||||
);
|
||||
|
||||
debug('Waiting for messages to come through');
|
||||
debug('Waiting for messages to regular contact to come through');
|
||||
await contactElem.waitFor();
|
||||
|
||||
await snapshot('main screen');
|
||||
await snapshot('styled bubbles');
|
||||
|
||||
debug('Going into the conversation');
|
||||
await contactElem.click();
|
||||
|
|
42
ts/test-node/util/rgbToHSL_test.ts
Normal file
42
ts/test-node/util/rgbToHSL_test.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { rgbToHSL } from '../../util/rgbToHSL';
|
||||
|
||||
describe('rgbToHSL', () => {
|
||||
it('converts pure rgb colors', () => {
|
||||
assert.deepStrictEqual(rgbToHSL(255, 0, 0), {
|
||||
h: 0,
|
||||
s: 1,
|
||||
l: 0.5,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(rgbToHSL(0, 255, 0), {
|
||||
h: 120,
|
||||
s: 1,
|
||||
l: 0.5,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(rgbToHSL(0, 0, 255), {
|
||||
h: 240,
|
||||
s: 1,
|
||||
l: 0.5,
|
||||
});
|
||||
});
|
||||
|
||||
it('converts random sampled rgb colors', () => {
|
||||
assert.deepStrictEqual(rgbToHSL(27, 132, 116), {
|
||||
h: 170.85714285714283,
|
||||
s: 0.6603773584905662,
|
||||
l: 0.31176470588235294,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(rgbToHSL(27, 175, 82), {
|
||||
h: 142.2972972972973,
|
||||
s: 0.7326732673267328,
|
||||
l: 0.396078431372549,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -170,12 +170,14 @@ export type ConversationColorType =
|
|||
| typeof ConversationColors[number]
|
||||
| 'custom';
|
||||
|
||||
export type CustomColorDataType = {
|
||||
id: string;
|
||||
value: CustomColorType;
|
||||
};
|
||||
|
||||
export type DefaultConversationColorType = {
|
||||
color: ConversationColorType;
|
||||
customColorData?: {
|
||||
id: string;
|
||||
value: CustomColorType;
|
||||
};
|
||||
customColorData?: CustomColorDataType;
|
||||
};
|
||||
|
||||
export const DEFAULT_CONVERSATION_COLOR: DefaultConversationColorType = {
|
||||
|
|
5
ts/types/Storage.d.ts
vendored
5
ts/types/Storage.d.ts
vendored
|
@ -64,6 +64,11 @@ export type StorageAccessType = {
|
|||
attachmentMigration_lastProcessedIndex: number;
|
||||
blocked: ReadonlyArray<string>;
|
||||
defaultConversationColor: DefaultConversationColorType;
|
||||
|
||||
// Not used UI, stored as is when imported from backup.
|
||||
defaultWallpaperPhotoPointer: Uint8Array;
|
||||
defaultWallpaperPreset: number;
|
||||
|
||||
customColors: CustomColorsItemType;
|
||||
device_name: string;
|
||||
existingOnboardingStoryMessageIds: ReadonlyArray<string> | undefined;
|
||||
|
|
26
ts/util/hslToRGB.ts
Normal file
26
ts/util/hslToRGB.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative
|
||||
export function hslToRGB(
|
||||
h: number,
|
||||
s: number,
|
||||
l: number
|
||||
): {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
} {
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
|
||||
function f(n: number): number {
|
||||
const k = (n + h / 30) % 12;
|
||||
return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
}
|
||||
|
||||
return {
|
||||
r: Math.round(255 * f(0)),
|
||||
g: Math.round(255 * f(8)),
|
||||
b: Math.round(255 * f(4)),
|
||||
};
|
||||
}
|
45
ts/util/rgbToHSL.ts
Normal file
45
ts/util/rgbToHSL.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
|
||||
export function rgbToHSL(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number
|
||||
): {
|
||||
h: number;
|
||||
s: number;
|
||||
l: number;
|
||||
} {
|
||||
// Normalize to [0, 1]
|
||||
const rn = r / 255;
|
||||
const gn = g / 255;
|
||||
const bn = b / 255;
|
||||
|
||||
const xMax = Math.max(rn, gn, bn);
|
||||
const xMin = Math.min(rn, gn, bn);
|
||||
const v = xMax;
|
||||
const c = xMax - xMin;
|
||||
const l = v - c / 2;
|
||||
let h: number;
|
||||
|
||||
if (c === 0) {
|
||||
h = 0;
|
||||
} else if (v === rn) {
|
||||
h = 60 * (((gn - bn) / c) % 6);
|
||||
} else if (v === gn) {
|
||||
h = 60 * ((bn - rn) / c + 2);
|
||||
} else {
|
||||
// v === b
|
||||
h = 60 * ((rn - gn) / c + 4);
|
||||
}
|
||||
|
||||
let s: number;
|
||||
if (l === 0 || l === 1) {
|
||||
s = 0;
|
||||
} else {
|
||||
s = (v - l) / Math.min(l, 1 - l);
|
||||
}
|
||||
|
||||
return { h, s, l };
|
||||
}
|
Loading…
Reference in a new issue