New attachment storage system

This commit is contained in:
Fedor Indutny 2024-07-11 12:44:09 -07:00 committed by GitHub
parent 273e1ccb15
commit 28664a606f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
161 changed files with 2418 additions and 1562 deletions

View file

@ -1,10 +1,16 @@
// Copyright 2018 Signal Messenger, LLC // Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ipcMain } from 'electron'; import { ipcMain, protocol } from 'electron';
import { createReadStream } from 'node:fs';
import { join, normalize } from 'node:path';
import { Readable, Transform, PassThrough } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import z from 'zod';
import * as rimraf from 'rimraf'; import * as rimraf from 'rimraf';
import { import {
getAllAttachments, getAllAttachments,
getAvatarsPath,
getPath, getPath,
getStickersPath, getStickersPath,
getTempPath, getTempPath,
@ -20,6 +26,11 @@ import type { MainSQL } from '../ts/sql/main';
import type { MessageAttachmentsCursorType } from '../ts/sql/Interface'; import type { MessageAttachmentsCursorType } from '../ts/sql/Interface';
import * as Errors from '../ts/types/errors'; import * as Errors from '../ts/types/errors';
import { sleep } from '../ts/util/sleep'; import { sleep } from '../ts/util/sleep';
import { isPathInside } from '../ts/util/isPathInside';
import { missingCaseError } from '../ts/util/missingCaseError';
import { safeParseInteger } from '../ts/util/numbers';
import { drop } from '../ts/util/drop';
import { decryptAttachmentV2ToSink } from '../ts/AttachmentCrypto';
let initialized = false; let initialized = false;
@ -31,6 +42,14 @@ const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
const INTERACTIVITY_DELAY = 50; const INTERACTIVITY_DELAY = 50;
const dispositionSchema = z.enum([
'attachment',
'temporary',
'draft',
'sticker',
'avatarData',
]);
type DeleteOrphanedAttachmentsOptionsType = Readonly<{ type DeleteOrphanedAttachmentsOptionsType = Readonly<{
orphanedAttachments: Set<string>; orphanedAttachments: Set<string>;
sql: MainSQL; sql: MainSQL;
@ -197,6 +216,7 @@ export function initialize({
const stickersDir = getStickersPath(configDir); const stickersDir = getStickersPath(configDir);
const tempDir = getTempPath(configDir); const tempDir = getTempPath(configDir);
const draftDir = getDraftPath(configDir); const draftDir = getDraftPath(configDir);
const avatarDataDir = getAvatarsPath(configDir);
ipcMain.handle(ERASE_TEMP_KEY, () => rimraf.sync(tempDir)); ipcMain.handle(ERASE_TEMP_KEY, () => rimraf.sync(tempDir));
ipcMain.handle(ERASE_ATTACHMENTS_KEY, () => rimraf.sync(attachmentsDir)); ipcMain.handle(ERASE_ATTACHMENTS_KEY, () => rimraf.sync(attachmentsDir));
@ -209,4 +229,216 @@ export function initialize({
const duration = Date.now() - start; const duration = Date.now() - start;
console.log(`cleanupOrphanedAttachments: took ${duration}ms`); console.log(`cleanupOrphanedAttachments: took ${duration}ms`);
}); });
protocol.handle('attachment', async req => {
const url = new URL(req.url);
if (url.host !== 'v1' && url.host !== 'v2') {
return new Response('Unknown host', { status: 404 });
}
// Disposition
let disposition: z.infer<typeof dispositionSchema> = 'attachment';
const dispositionParam = url.searchParams.get('disposition');
if (dispositionParam != null) {
disposition = dispositionSchema.parse(dispositionParam);
}
let parentDir: string;
switch (disposition) {
case 'attachment':
parentDir = attachmentsDir;
break;
case 'temporary':
parentDir = tempDir;
break;
case 'draft':
parentDir = draftDir;
break;
case 'sticker':
parentDir = stickersDir;
break;
case 'avatarData':
parentDir = avatarDataDir;
break;
default:
throw missingCaseError(disposition);
}
// Remove first slash
const path = normalize(
join(parentDir, ...url.pathname.slice(1).split(/\//g))
);
if (!isPathInside(path, parentDir)) {
return new Response('Access denied', { status: 401 });
}
// Get attachment size to trim the padding
const sizeParam = url.searchParams.get('size');
let maybeSize: number | undefined;
if (sizeParam != null) {
const intValue = safeParseInteger(sizeParam);
if (intValue != null) {
maybeSize = intValue;
}
}
// Legacy plaintext attachments
if (url.host === 'v1') {
return handleRangeRequest({
request: req,
size: maybeSize,
plaintext: createReadStream(path),
});
}
// Encrypted attachments
// Get AES+MAC key
const maybeKeysBase64 = url.searchParams.get('key');
if (maybeKeysBase64 == null) {
return new Response('Missing key', { status: 400 });
}
// Size is required for trimming padding.
if (maybeSize == null) {
return new Response('Missing size', { status: 400 });
}
// Pacify typescript
const size = maybeSize;
const keysBase64 = maybeKeysBase64;
const plaintext = new PassThrough();
async function runSafe(): Promise<void> {
try {
await decryptAttachmentV2ToSink(
{
ciphertextPath: path,
idForLogging: 'attachment_channel',
keysBase64,
size,
isLocal: true,
},
plaintext
);
} catch (error) {
plaintext.emit('error', error);
}
}
drop(runSafe());
return handleRangeRequest({
request: req,
size: maybeSize,
plaintext,
});
});
}
type HandleRangeRequestOptionsType = Readonly<{
request: Request;
size: number | undefined;
plaintext: Readable;
}>;
function handleRangeRequest({
request,
size,
plaintext,
}: HandleRangeRequestOptionsType): Response {
const url = new URL(request.url);
// Get content-type
const contentType = url.searchParams.get('contentType');
const headers: HeadersInit = {
'cache-control': 'no-cache, no-store',
'content-type': contentType || 'application/octet-stream',
};
if (size != null) {
headers['content-length'] = size.toString();
}
const create200Response = (): Response => {
return new Response(Readable.toWeb(plaintext) as ReadableStream<Buffer>, {
status: 200,
headers,
});
};
const range = request.headers.get('range');
if (range == null) {
return create200Response();
}
const match = range.match(/^bytes=(\d+)-(\d+)?$/);
if (match == null) {
console.error(`attachment_channel: invalid range header: ${range}`);
return create200Response();
}
const startParam = safeParseInteger(match[1]);
if (startParam == null) {
console.error(`attachment_channel: invalid range header: ${range}`);
return create200Response();
}
let endParam: number | undefined;
if (match[2] != null) {
const intValue = safeParseInteger(match[2]);
if (intValue == null) {
console.error(`attachment_channel: invalid range header: ${range}`);
return create200Response();
}
endParam = intValue;
}
const start = Math.min(startParam, size || Infinity);
let end: number;
if (endParam === undefined) {
end = size || Infinity;
} else {
// Supplied range is inclusive
end = Math.min(endParam + 1, size || Infinity);
}
let offset = 0;
const transform = new Transform({
transform(data, _enc, callback) {
if (offset + data.byteLength >= start && offset <= end) {
this.push(data.subarray(Math.max(0, start - offset), end - offset));
}
offset += data.byteLength;
callback();
},
});
headers['content-range'] =
size === undefined
? `bytes ${start}-${endParam === undefined ? '' : end - 1}/*`
: `bytes ${start}-${end - 1}/${size}`;
if (endParam !== undefined || size !== undefined) {
headers['content-length'] = (end - start).toString();
}
drop(
(async () => {
try {
await pipeline(plaintext, transform);
} catch (error) {
transform.emit('error', error);
}
})()
);
return new Response(Readable.toWeb(transform) as ReadableStream<Buffer>, {
status: 206,
headers,
});
} }

View file

@ -1,12 +1,19 @@
// Copyright 2018 Signal Messenger, LLC // Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { PassThrough } from 'node:stream';
import { join, relative, normalize } from 'path'; import { join, relative, normalize } from 'path';
import fastGlob from 'fast-glob'; import fastGlob from 'fast-glob';
import fse from 'fs-extra'; import fse from 'fs-extra';
import { map, isString } from 'lodash'; import { map, isString } from 'lodash';
import normalizePath from 'normalize-path'; import normalizePath from 'normalize-path';
import { isPathInside } from '../ts/util/isPathInside'; import { isPathInside } from '../ts/util/isPathInside';
import {
generateKeys,
decryptAttachmentV2ToSink,
encryptAttachmentV2ToDisk,
} from '../ts/AttachmentCrypto';
import type { LocalAttachmentV2Type } from '../ts/types/Attachment';
const PATH = 'attachments.noindex'; const PATH = 'attachments.noindex';
const AVATAR_PATH = 'avatars.noindex'; const AVATAR_PATH = 'avatars.noindex';
@ -190,3 +197,57 @@ export const deleteAllDraftAttachments = async ({
console.log(`deleteAllDraftAttachments: deleted ${attachments.length} files`); console.log(`deleteAllDraftAttachments: deleted ${attachments.length} files`);
}; };
export const readAndDecryptDataFromDisk = async ({
absolutePath,
keysBase64,
size,
}: {
absolutePath: string;
keysBase64: string;
size: number;
}): Promise<Uint8Array> => {
const sink = new PassThrough();
const chunks = new Array<Buffer>();
sink.on('data', chunk => chunks.push(chunk));
sink.resume();
await decryptAttachmentV2ToSink(
{
ciphertextPath: absolutePath,
idForLogging: 'attachments/readAndDecryptDataFromDisk',
keysBase64,
size,
isLocal: true,
},
sink
);
return Buffer.concat(chunks);
};
export const writeNewAttachmentData = async ({
data,
getAbsoluteAttachmentPath,
}: {
data: Uint8Array;
getAbsoluteAttachmentPath: (relativePath: string) => string;
}): Promise<LocalAttachmentV2Type> => {
const keys = generateKeys();
const { plaintextHash, path } = await encryptAttachmentV2ToDisk({
plaintext: { data },
getAbsoluteAttachmentPath,
keys,
});
return {
version: 2,
plaintextHash,
size: data.byteLength,
path,
localKey: Buffer.from(keys).toString('base64'),
};
};

View file

@ -29,6 +29,7 @@ import {
systemPreferences, systemPreferences,
Notification, Notification,
safeStorage, safeStorage,
protocol as electronProtocol,
} from 'electron'; } from 'electron';
import type { MenuItemConstructorOptions, Settings } from 'electron'; import type { MenuItemConstructorOptions, Settings } from 'electron';
import { z } from 'zod'; import { z } from 'zod';
@ -1823,6 +1824,17 @@ if (DISABLE_GPU) {
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
} }
// This has to run before the 'ready' event.
electronProtocol.registerSchemesAsPrivileged([
{
scheme: 'attachment',
privileges: {
supportFetchAPI: true,
stream: true,
},
},
]);
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.

View file

@ -16,12 +16,12 @@
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'none'; content="default-src 'none';
child-src 'self'; child-src 'self';
connect-src 'self' https: wss:; connect-src 'self' https: wss: attachment:;
font-src 'self'; font-src 'self';
form-action 'self'; form-action 'self';
frame-src 'none'; frame-src 'none';
img-src 'self' blob: data: emoji:; img-src 'self' blob: data: emoji: attachment:;
media-src 'self' blob:; media-src 'self' blob: attachment:;
object-src 'none'; object-src 'none';
script-src 'self' 'sha256-Qu05oqDmBO5fZacm7tr/oerJcqsW0G/XqP4PRCziovc=' 'sha256-eLeGwSfPmXJ+EUiLfIeXABvLiUqDbiKgNLpHITaabgQ='; script-src 'self' 'sha256-Qu05oqDmBO5fZacm7tr/oerJcqsW0G/XqP4PRCziovc=' 'sha256-eLeGwSfPmXJ+EUiLfIeXABvLiUqDbiKgNLpHITaabgQ=';
style-src 'self' 'unsafe-inline';" style-src 'self' 'unsafe-inline';"

View file

@ -3,45 +3,44 @@
import { unlinkSync, createReadStream, createWriteStream } from 'fs'; import { unlinkSync, createReadStream, createWriteStream } from 'fs';
import { open } from 'fs/promises'; import { open } from 'fs/promises';
import { import { createCipheriv, createHash, createHmac, randomBytes } from 'crypto';
createDecipheriv, import type { Hash } from 'crypto';
createCipheriv,
createHash,
createHmac,
randomBytes,
} from 'crypto';
import type { Decipher, Hash, Hmac } from 'crypto';
import { PassThrough, Transform, type Writable, Readable } from 'stream'; import { PassThrough, Transform, type Writable, Readable } from 'stream';
import { pipeline } from 'stream/promises'; import { pipeline } from 'stream/promises';
import { ensureFile } from 'fs-extra'; import { ensureFile } from 'fs-extra';
import * as log from './logging/log'; import * as log from './logging/log';
import { HashType, CipherType } from './types/Crypto'; import {
import { createName, getRelativePath } from './windows/attachments'; HashType,
CipherType,
IV_LENGTH,
KEY_LENGTH,
MAC_LENGTH,
} from './types/Crypto';
import { constantTimeEqual } from './Crypto'; import { constantTimeEqual } from './Crypto';
import { createName, getRelativePath } from './util/attachmentPath';
import { appendPaddingStream, logPadSize } from './util/logPadding'; import { appendPaddingStream, logPadSize } from './util/logPadding';
import { prependStream } from './util/prependStream'; import { prependStream } from './util/prependStream';
import { appendMacStream } from './util/appendMacStream'; import { appendMacStream } from './util/appendMacStream';
import { Environment } from './environment'; import { getIvAndDecipher } from './util/getIvAndDecipher';
import type { AttachmentType } from './types/Attachment'; import { getMacAndUpdateHmac } from './util/getMacAndUpdateHmac';
import type { ContextType } from './types/Message2'; import { trimPadding } from './util/trimPadding';
import { strictAssert } from './util/assert'; import { strictAssert } from './util/assert';
import * as Errors from './types/errors'; import * as Errors from './types/errors';
import { isNotNil } from './util/isNotNil'; import { isNotNil } from './util/isNotNil';
import { missingCaseError } from './util/missingCaseError'; import { missingCaseError } from './util/missingCaseError';
import { getEnvironment, Environment } from './environment';
// This file was split from ts/Crypto.ts because it pulls things in from node, and // This file was split from ts/Crypto.ts because it pulls things in from node, and
// too many things pull in Crypto.ts, so it broke storybook. // too many things pull in Crypto.ts, so it broke storybook.
const IV_LENGTH = 16; const DIGEST_LENGTH = MAC_LENGTH;
const KEY_LENGTH = 32;
const DIGEST_LENGTH = 32;
const HEX_DIGEST_LENGTH = DIGEST_LENGTH * 2; const HEX_DIGEST_LENGTH = DIGEST_LENGTH * 2;
const ATTACHMENT_MAC_LENGTH = 32; const ATTACHMENT_MAC_LENGTH = MAC_LENGTH;
export class ReencyptedDigestMismatchError extends Error {} export class ReencyptedDigestMismatchError extends Error {}
/** @private */ /** @private */
export const KEY_SET_LENGTH = KEY_LENGTH + ATTACHMENT_MAC_LENGTH; export const KEY_SET_LENGTH = KEY_LENGTH + MAC_LENGTH;
export function _generateAttachmentIv(): Uint8Array { export function _generateAttachmentIv(): Uint8Array {
return randomBytes(IV_LENGTH); return randomBytes(IV_LENGTH);
@ -58,14 +57,31 @@ export type EncryptedAttachmentV2 = {
ciphertextSize: number; ciphertextSize: number;
}; };
export type ReencryptedAttachmentV2 = {
path: string;
iv: Uint8Array;
plaintextHash: string;
key: Uint8Array;
};
export type DecryptedAttachmentV2 = { export type DecryptedAttachmentV2 = {
path: string; path: string;
iv: Uint8Array; iv: Uint8Array;
plaintextHash: string; plaintextHash: string;
}; };
export type ReecryptedAttachmentV2 = {
key: Uint8Array;
mac: Uint8Array;
path: string;
iv: Uint8Array;
plaintextHash: string;
};
export type PlaintextSourceType = export type PlaintextSourceType =
| { data: Uint8Array } | { data: Uint8Array }
| { stream: Readable }
| { absolutePath: string }; | { absolutePath: string };
export type HardcodedIVForEncryptionType = export type HardcodedIVForEncryptionType =
@ -84,6 +100,7 @@ type EncryptAttachmentV2PropsType = {
keys: Readonly<Uint8Array>; keys: Readonly<Uint8Array>;
dangerousIv?: HardcodedIVForEncryptionType; dangerousIv?: HardcodedIVForEncryptionType;
dangerousTestOnlySkipPadding?: boolean; dangerousTestOnlySkipPadding?: boolean;
getAbsoluteAttachmentPath: (relativePath: string) => string;
}; };
export async function encryptAttachmentV2ToDisk( export async function encryptAttachmentV2ToDisk(
@ -91,8 +108,7 @@ export async function encryptAttachmentV2ToDisk(
): Promise<EncryptedAttachmentV2 & { path: string }> { ): Promise<EncryptedAttachmentV2 & { path: string }> {
// Create random output file // Create random output file
const relativeTargetPath = getRelativePath(createName()); const relativeTargetPath = getRelativePath(createName());
const absoluteTargetPath = const absoluteTargetPath = args.getAbsoluteAttachmentPath(relativeTargetPath);
window.Signal.Migrations.getAbsoluteAttachmentPath(relativeTargetPath);
await ensureFile(absoluteTargetPath); await ensureFile(absoluteTargetPath);
@ -128,7 +144,7 @@ export async function encryptAttachmentV2({
if (dangerousIv) { if (dangerousIv) {
if (dangerousIv.reason === 'test') { if (dangerousIv.reason === 'test') {
if (window.getEnvironment() !== Environment.Test) { if (getEnvironment() !== Environment.Test) {
throw new Error( throw new Error(
`${logId}: Used dangerousIv with reason test outside tests!` `${logId}: Used dangerousIv with reason test outside tests!`
); );
@ -146,10 +162,7 @@ export async function encryptAttachmentV2({
} }
} }
if ( if (dangerousTestOnlySkipPadding && getEnvironment() !== Environment.Test) {
dangerousTestOnlySkipPadding &&
window.getEnvironment() !== Environment.Test
) {
throw new Error( throw new Error(
`${logId}: Used dangerousTestOnlySkipPadding outside tests!` `${logId}: Used dangerousTestOnlySkipPadding outside tests!`
); );
@ -160,12 +173,17 @@ export async function encryptAttachmentV2({
const digest = createHash(HashType.size256); const digest = createHash(HashType.size256);
let ciphertextSize: number | undefined; let ciphertextSize: number | undefined;
let mac: Uint8Array | undefined;
try { try {
const source = let source: Readable;
'data' in plaintext if ('data' in plaintext) {
? Readable.from([Buffer.from(plaintext.data)]) source = Readable.from([Buffer.from(plaintext.data)]);
: createReadStream(plaintext.absolutePath); } else if ('stream' in plaintext) {
source = plaintext.stream;
} else {
source = createReadStream(plaintext.absolutePath);
}
await pipeline( await pipeline(
[ [
@ -174,7 +192,9 @@ export async function encryptAttachmentV2({
dangerousTestOnlySkipPadding ? undefined : appendPaddingStream(), dangerousTestOnlySkipPadding ? undefined : appendPaddingStream(),
createCipheriv(CipherType.AES256CBC, aesKey, iv), createCipheriv(CipherType.AES256CBC, aesKey, iv),
prependIv(iv), prependIv(iv),
appendMacStream(macKey), appendMacStream(macKey, macValue => {
mac = macValue;
}),
peekAndUpdateHash(digest), peekAndUpdateHash(digest),
measureSize(size => { measureSize(size => {
ciphertextSize = size; ciphertextSize = size;
@ -204,6 +224,7 @@ export async function encryptAttachmentV2({
); );
strictAssert(ciphertextSize != null, 'Failed to measure ciphertext size!'); strictAssert(ciphertextSize != null, 'Failed to measure ciphertext size!');
strictAssert(mac != null, 'Failed to compute mac!');
if (dangerousIv?.reason === 'reencrypting-for-backup') { if (dangerousIv?.reason === 'reencrypting-for-backup') {
if (!constantTimeEqual(ourDigest, dangerousIv.digestToMatch)) { if (!constantTimeEqual(ourDigest, dangerousIv.digestToMatch)) {
@ -221,37 +242,102 @@ export async function encryptAttachmentV2({
}; };
} }
type DecryptAttachmentOptionsType = Readonly<{ type DecryptAttachmentToSinkOptionsType = Readonly<
{
ciphertextPath: string; ciphertextPath: string;
idForLogging: string; idForLogging: string;
aesKey: Readonly<Uint8Array>;
macKey: Readonly<Uint8Array>;
size: number; size: number;
theirDigest: Readonly<Uint8Array>;
outerEncryption?: { outerEncryption?: {
aesKey: Readonly<Uint8Array>; aesKey: Readonly<Uint8Array>;
macKey: Readonly<Uint8Array>; macKey: Readonly<Uint8Array>;
}; };
} & (
| {
isLocal?: false;
theirDigest: Readonly<Uint8Array>;
}
| {
// No need to check integrity for already downloaded attachments
isLocal: true;
theirDigest?: undefined;
}
) &
(
| {
aesKey: Readonly<Uint8Array>;
macKey: Readonly<Uint8Array>;
}
| {
// The format used by most stored attachments
keysBase64: string;
}
)
>;
export type DecryptAttachmentOptionsType = DecryptAttachmentToSinkOptionsType &
Readonly<{
getAbsoluteAttachmentPath: (relativePath: string) => string;
}>; }>;
export async function decryptAttachmentV2( export async function decryptAttachmentV2(
options: DecryptAttachmentOptionsType options: DecryptAttachmentOptionsType
): Promise<DecryptedAttachmentV2> { ): Promise<DecryptedAttachmentV2> {
const { const logId = `decryptAttachmentV2(${options.idForLogging})`;
idForLogging,
macKey,
aesKey,
ciphertextPath,
theirDigest,
outerEncryption,
} = options;
const logId = `decryptAttachmentV2(${idForLogging})`;
// Create random output file // Create random output file
const relativeTargetPath = getRelativePath(createName()); const relativeTargetPath = getRelativePath(createName());
const absoluteTargetPath = const absoluteTargetPath =
window.Signal.Migrations.getAbsoluteAttachmentPath(relativeTargetPath); options.getAbsoluteAttachmentPath(relativeTargetPath);
let writeFd;
try {
try {
await ensureFile(absoluteTargetPath);
writeFd = await open(absoluteTargetPath, 'w');
} catch (cause) {
throw new Error(`${logId}: Failed to create write path`, { cause });
}
const result = await decryptAttachmentV2ToSink(
options,
writeFd.createWriteStream()
);
return {
...result,
path: relativeTargetPath,
};
} catch (error) {
log.error(
`${logId}: Failed to decrypt attachment to disk`,
Errors.toLogFormat(error)
);
safeUnlinkSync(absoluteTargetPath);
throw error;
} finally {
await writeFd?.close();
}
}
export async function decryptAttachmentV2ToSink(
options: DecryptAttachmentToSinkOptionsType,
sink: Writable
): Promise<Omit<DecryptedAttachmentV2, 'path'>> {
const { idForLogging, ciphertextPath, outerEncryption } = options;
let aesKey: Uint8Array;
let macKey: Uint8Array;
if ('aesKey' in options) {
({ aesKey, macKey } = options);
} else {
const { keysBase64 } = options;
const keys = Buffer.from(keysBase64, 'base64');
({ aesKey, macKey } = splitKeys(keys));
}
const logId = `decryptAttachmentV2(${idForLogging})`;
const digest = createHash(HashType.size256); const digest = createHash(HashType.size256);
const hmac = createHmac(HashType.size256, macKey); const hmac = createHmac(HashType.size256, macKey);
@ -276,7 +362,6 @@ export async function decryptAttachmentV2(
: undefined; : undefined;
let readFd; let readFd;
let writeFd;
let iv: Uint8Array | undefined; let iv: Uint8Array | undefined;
try { try {
try { try {
@ -284,12 +369,6 @@ export async function decryptAttachmentV2(
} catch (cause) { } catch (cause) {
throw new Error(`${logId}: Read path doesn't exist`, { cause }); throw new Error(`${logId}: Read path doesn't exist`, { cause });
} }
try {
await ensureFile(absoluteTargetPath);
writeFd = await open(absoluteTargetPath, 'w');
} catch (cause) {
throw new Error(`${logId}: Failed to create write path`, { cause });
}
await pipeline( await pipeline(
[ [
@ -305,18 +384,23 @@ export async function decryptAttachmentV2(
}), }),
trimPadding(options.size), trimPadding(options.size),
peekAndUpdateHash(plaintextHash), peekAndUpdateHash(plaintextHash),
writeFd.createWriteStream(), sink,
].filter(isNotNil) ].filter(isNotNil)
); );
} catch (error) { } catch (error) {
// These errors happen when canceling fetch from `attachment://` urls,
// ignore them to avoid noise in the logs.
if (error.name === 'AbortError') {
throw error;
}
log.error( log.error(
`${logId}: Failed to decrypt attachment`, `${logId}: Failed to decrypt attachment`,
Errors.toLogFormat(error) Errors.toLogFormat(error)
); );
safeUnlinkSync(absoluteTargetPath);
throw error; throw error;
} finally { } finally {
await Promise.all([readFd?.close(), writeFd?.close()]); await readFd?.close();
} }
const ourMac = hmac.digest(); const ourMac = hmac.digest();
@ -343,7 +427,7 @@ export async function decryptAttachmentV2(
if (!constantTimeEqual(ourMac, theirMac)) { if (!constantTimeEqual(ourMac, theirMac)) {
throw new Error(`${logId}: Bad MAC`); throw new Error(`${logId}: Bad MAC`);
} }
if (!constantTimeEqual(ourDigest, theirDigest)) { if (!options.isLocal && !constantTimeEqual(ourDigest, options.theirDigest)) {
throw new Error(`${logId}: Bad digest`); throw new Error(`${logId}: Bad digest`);
} }
@ -372,12 +456,64 @@ export async function decryptAttachmentV2(
} }
return { return {
path: relativeTargetPath,
iv, iv,
plaintextHash: ourPlaintextHash, plaintextHash: ourPlaintextHash,
}; };
} }
export async function reencryptAttachmentV2(
options: DecryptAttachmentOptionsType
): Promise<ReencryptedAttachmentV2> {
const { idForLogging } = options;
const logId = `reencryptAttachmentV2(${idForLogging})`;
// Create random output file
const relativeTargetPath = getRelativePath(createName());
const absoluteTargetPath =
options.getAbsoluteAttachmentPath(relativeTargetPath);
let writeFd;
try {
try {
await ensureFile(absoluteTargetPath);
writeFd = await open(absoluteTargetPath, 'w');
} catch (cause) {
throw new Error(`${logId}: Failed to create write path`, { cause });
}
const keys = generateKeys();
const passthrough = new PassThrough();
const [result] = await Promise.all([
decryptAttachmentV2ToSink(options, passthrough),
await encryptAttachmentV2({
keys,
plaintext: {
stream: passthrough,
},
sink: createWriteStream(absoluteTargetPath),
getAbsoluteAttachmentPath: options.getAbsoluteAttachmentPath,
}),
]);
return {
...result,
key: keys,
path: relativeTargetPath,
};
} catch (error) {
log.error(
`${logId}: Failed to decrypt attachment`,
Errors.toLogFormat(error)
);
safeUnlinkSync(absoluteTargetPath);
throw error;
} finally {
await writeFd?.close();
}
}
/** /**
* Splits the keys into aes and mac keys. * Splits the keys into aes and mac keys.
*/ */
@ -396,6 +532,10 @@ export function splitKeys(keys: Uint8Array): AttachmentEncryptionKeysType {
return { aesKey, macKey }; return { aesKey, macKey };
} }
export function generateKeys(): Uint8Array {
return randomBytes(KEY_SET_LENGTH);
}
/** /**
* Updates a hash of the stream without modifying it. * Updates a hash of the stream without modifying it.
*/ */
@ -412,122 +552,6 @@ function peekAndUpdateHash(hash: Hash) {
}); });
} }
/**
* Updates an hmac with the stream except for the last ATTACHMENT_MAC_LENGTH
* bytes. The last ATTACHMENT_MAC_LENGTH bytes are passed to the callback.
*/
export function getMacAndUpdateHmac(
hmac: Hmac,
onTheirMac: (theirMac: Uint8Array) => void
): Transform {
// Because we don't have a view of the entire stream, we don't know when we're
// at the end. We need to omit the last ATTACHMENT_MAC_LENGTH bytes from
// `hmac.update` so we only push what we know is not the mac.
let maybeMacBytes = Buffer.alloc(0);
function updateWithKnownNonMacBytes() {
let knownNonMacBytes = null;
if (maybeMacBytes.byteLength > ATTACHMENT_MAC_LENGTH) {
knownNonMacBytes = maybeMacBytes.subarray(0, -ATTACHMENT_MAC_LENGTH);
maybeMacBytes = maybeMacBytes.subarray(-ATTACHMENT_MAC_LENGTH);
hmac.update(knownNonMacBytes);
}
return knownNonMacBytes;
}
return new Transform({
transform(chunk, _encoding, callback) {
try {
maybeMacBytes = Buffer.concat([maybeMacBytes, chunk]);
const knownNonMac = updateWithKnownNonMacBytes();
callback(null, knownNonMac);
} catch (error) {
callback(error);
}
},
flush(callback) {
try {
onTheirMac(maybeMacBytes);
callback(null, null);
} catch (error) {
callback(error);
}
},
});
}
/**
* Gets the IV from the start of the stream and creates a decipher.
* Then deciphers the rest of the stream.
*/
export function getIvAndDecipher(
aesKey: Uint8Array,
onFoundIv?: (iv: Buffer) => void
): Transform {
let maybeIvBytes: Buffer | null = Buffer.alloc(0);
let decipher: Decipher | null = null;
return new Transform({
transform(chunk, _encoding, callback) {
try {
// If we've already initialized the decipher, just pass the chunk through.
if (decipher != null) {
callback(null, decipher.update(chunk));
return;
}
// Wait until we have enough bytes to get the iv to initialize the
// decipher.
maybeIvBytes = Buffer.concat([maybeIvBytes, chunk]);
if (maybeIvBytes.byteLength < IV_LENGTH) {
callback(null, null);
return;
}
// Once we have enough bytes, initialize the decipher and pass the
// remainder of the bytes through.
const iv = maybeIvBytes.subarray(0, IV_LENGTH);
const remainder = maybeIvBytes.subarray(IV_LENGTH);
onFoundIv?.(iv);
maybeIvBytes = null; // free memory
decipher = createDecipheriv(CipherType.AES256CBC, aesKey, iv);
callback(null, decipher.update(remainder));
} catch (error) {
callback(error);
}
},
flush(callback) {
try {
strictAssert(decipher != null, 'decipher must be set');
callback(null, decipher.final());
} catch (error) {
callback(error);
}
},
});
}
/**
* Truncates the stream to the target size.
*/
function trimPadding(size: number) {
let total = 0;
return new Transform({
transform(chunk, _encoding, callback) {
const chunkSize = chunk.byteLength;
const sizeLeft = size - total;
if (sizeLeft >= chunkSize) {
total += chunkSize;
callback(null, chunk);
} else if (sizeLeft > 0) {
total += sizeLeft;
callback(null, chunk.subarray(0, sizeLeft));
} else {
callback(null, null);
}
},
});
}
export function measureSize(onComplete: (size: number) => void): Transform { export function measureSize(onComplete: (size: number) => void): Transform {
let totalBytes = 0; let totalBytes = 0;
const passthrough = new PassThrough(); const passthrough = new PassThrough();
@ -568,62 +592,6 @@ function prependIv(iv: Uint8Array) {
return prependStream(iv); return prependStream(iv);
} }
/**
* Called during message schema migration. New messages downloaded should have
* plaintextHash added automatically during decryption / writing to file system.
*/
export async function addPlaintextHashToAttachment(
attachment: AttachmentType,
{ getAbsoluteAttachmentPath }: ContextType
): Promise<AttachmentType> {
if (!attachment.path) {
return attachment;
}
const plaintextHash = await getPlaintextHashForAttachmentOnDisk(
getAbsoluteAttachmentPath(attachment.path)
);
if (!plaintextHash) {
log.error('addPlaintextHashToAttachment: Failed to generate hash');
return attachment;
}
return {
...attachment,
plaintextHash,
};
}
export async function getPlaintextHashForAttachmentOnDisk(
absolutePath: string
): Promise<string | undefined> {
let readFd;
try {
try {
readFd = await open(absolutePath, 'r');
} catch (error) {
log.error('addPlaintextHashToAttachment: Target path does not exist');
return undefined;
}
const hash = createHash(HashType.size256);
await pipeline(readFd.createReadStream(), hash);
const plaintextHash = hash.digest('hex');
if (!plaintextHash) {
log.error(
'addPlaintextHashToAttachment: no hash generated from file; is the file empty?'
);
return;
}
return plaintextHash;
} catch (error) {
log.error('addPlaintextHashToAttachment: error during file read', error);
return undefined;
} finally {
await readFd?.close();
}
}
export function getPlaintextHashForInMemoryAttachment( export function getPlaintextHashForInMemoryAttachment(
data: Uint8Array data: Uint8Array
): string { ): string {

View file

@ -208,6 +208,7 @@ import type { ReadSyncTaskType } from './messageModifiers/ReadSyncs';
import { isEnabled } from './RemoteConfig'; import { isEnabled } from './RemoteConfig';
import { AttachmentBackupManager } from './jobs/AttachmentBackupManager'; import { AttachmentBackupManager } from './jobs/AttachmentBackupManager';
import { getConversationIdForLogging } from './util/idForLogging'; import { getConversationIdForLogging } from './util/idForLogging';
import { encryptConversationAttachments } from './util/encryptConversationAttachments';
export function isOverHourIntoPast(timestamp: number): boolean { export function isOverHourIntoPast(timestamp: number): boolean {
return isNumber(timestamp) && isOlderThan(timestamp, HOUR); return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
@ -960,12 +961,22 @@ export async function startApp(): Promise<void> {
window.i18n window.i18n
); );
if (newVersion) { if (newVersion || window.storage.get('needOrphanedAttachmentCheck')) {
await window.storage.remove('needOrphanedAttachmentCheck');
await window.Signal.Data.cleanupOrphanedAttachments(); await window.Signal.Data.cleanupOrphanedAttachments();
drop(window.Signal.Data.ensureFilePermissions()); drop(window.Signal.Data.ensureFilePermissions());
} }
if (
newVersion &&
lastVersion &&
window.isBeforeVersion(lastVersion, 'v7.18.0-beta.1')
) {
await encryptConversationAttachments();
await Stickers.encryptLegacyStickers();
}
setAppLoadingScreenMessage(window.i18n('icu:loading'), window.i18n); setAppLoadingScreenMessage(window.i18n('icu:loading'), window.i18n);
let isMigrationWithIndexComplete = false; let isMigrationWithIndexComplete = false;

View file

@ -129,7 +129,7 @@ export function AddUserToAnotherGroupModal({
} }
return { return {
...pick(convo, 'id', 'avatarPath', 'title', 'unblurredAvatarPath'), ...pick(convo, 'id', 'avatarUrl', 'title', 'unblurredAvatarUrl'),
memberships, memberships,
membersCount, membersCount,
disabledReason, disabledReason,

View file

@ -65,7 +65,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
acceptedMessageRequest: isBoolean(overrideProps.acceptedMessageRequest) acceptedMessageRequest: isBoolean(overrideProps.acceptedMessageRequest)
? overrideProps.acceptedMessageRequest ? overrideProps.acceptedMessageRequest
: true, : true,
avatarPath: overrideProps.avatarPath || '', avatarUrl: overrideProps.avatarUrl || '',
badge: overrideProps.badge, badge: overrideProps.badge,
blur: overrideProps.blur, blur: overrideProps.blur,
color: overrideProps.color || AvatarColors[0], color: overrideProps.color || AvatarColors[0],
@ -107,7 +107,7 @@ const TemplateSingle: StoryFn<Props> = (args: Props) => (
export const Default = Template.bind({}); export const Default = Template.bind({});
Default.args = createProps({ Default.args = createProps({
avatarPath: '/fixtures/giphy-GVNvOUpeYmI7e.gif', avatarUrl: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
Default.play = async (context: any) => { Default.play = async (context: any) => {
@ -120,13 +120,13 @@ Default.play = async (context: any) => {
export const WithBadge = Template.bind({}); export const WithBadge = Template.bind({});
WithBadge.args = createProps({ WithBadge.args = createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg', avatarUrl: '/fixtures/kitten-3-64-64.jpg',
badge: getFakeBadge(), badge: getFakeBadge(),
}); });
export const WideImage = Template.bind({}); export const WideImage = Template.bind({});
WideImage.args = createProps({ WideImage.args = createProps({
avatarPath: '/fixtures/wide.jpg', avatarUrl: '/fixtures/wide.jpg',
}); });
export const OneWordName = Template.bind({}); export const OneWordName = Template.bind({});
@ -186,12 +186,12 @@ BrokenColor.args = createProps({
export const BrokenAvatar = Template.bind({}); export const BrokenAvatar = Template.bind({});
BrokenAvatar.args = createProps({ BrokenAvatar.args = createProps({
avatarPath: 'badimage.png', avatarUrl: 'badimage.png',
}); });
export const BrokenAvatarForGroup = Template.bind({}); export const BrokenAvatarForGroup = Template.bind({});
BrokenAvatarForGroup.args = createProps({ BrokenAvatarForGroup.args = createProps({
avatarPath: 'badimage.png', avatarUrl: 'badimage.png',
conversationType: 'group', conversationType: 'group',
}); });
@ -203,29 +203,29 @@ Loading.args = createProps({
export const BlurredBasedOnProps = TemplateSingle.bind({}); export const BlurredBasedOnProps = TemplateSingle.bind({});
BlurredBasedOnProps.args = createProps({ BlurredBasedOnProps.args = createProps({
acceptedMessageRequest: false, acceptedMessageRequest: false,
avatarPath: '/fixtures/kitten-3-64-64.jpg', avatarUrl: '/fixtures/kitten-3-64-64.jpg',
}); });
export const ForceBlurred = TemplateSingle.bind({}); export const ForceBlurred = TemplateSingle.bind({});
ForceBlurred.args = createProps({ ForceBlurred.args = createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg', avatarUrl: '/fixtures/kitten-3-64-64.jpg',
blur: AvatarBlur.BlurPicture, blur: AvatarBlur.BlurPicture,
}); });
export const BlurredWithClickToView = TemplateSingle.bind({}); export const BlurredWithClickToView = TemplateSingle.bind({});
BlurredWithClickToView.args = createProps({ BlurredWithClickToView.args = createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg', avatarUrl: '/fixtures/kitten-3-64-64.jpg',
blur: AvatarBlur.BlurPictureWithClickToView, blur: AvatarBlur.BlurPictureWithClickToView,
}); });
export const StoryUnread = TemplateSingle.bind({}); export const StoryUnread = TemplateSingle.bind({});
StoryUnread.args = createProps({ StoryUnread.args = createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg', avatarUrl: '/fixtures/kitten-3-64-64.jpg',
storyRing: HasStories.Unread, storyRing: HasStories.Unread,
}); });
export const StoryRead = TemplateSingle.bind({}); export const StoryRead = TemplateSingle.bind({});
StoryRead.args = createProps({ StoryRead.args = createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg', avatarUrl: '/fixtures/kitten-3-64-64.jpg',
storyRing: HasStories.Read, storyRing: HasStories.Read,
}); });

View file

@ -53,7 +53,7 @@ export enum AvatarSize {
type BadgePlacementType = { bottom: number; right: number }; type BadgePlacementType = { bottom: number; right: number };
export type Props = { export type Props = {
avatarPath?: string; avatarUrl?: string;
blur?: AvatarBlur; blur?: AvatarBlur;
color?: AvatarColorType; color?: AvatarColorType;
loading?: boolean; loading?: boolean;
@ -67,7 +67,7 @@ export type Props = {
sharedGroupNames: ReadonlyArray<string>; sharedGroupNames: ReadonlyArray<string>;
size: AvatarSize; size: AvatarSize;
title: string; title: string;
unblurredAvatarPath?: string; unblurredAvatarUrl?: string;
searchResult?: boolean; searchResult?: boolean;
storyRing?: HasStories; storyRing?: HasStories;
@ -107,7 +107,7 @@ const getDefaultBlur = (
export function Avatar({ export function Avatar({
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
badge, badge,
className, className,
color = 'A200', color = 'A200',
@ -123,15 +123,15 @@ export function Avatar({
size, size,
theme, theme,
title, title,
unblurredAvatarPath, unblurredAvatarUrl,
searchResult, searchResult,
storyRing, storyRing,
blur = getDefaultBlur({ blur = getDefaultBlur({
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
isMe, isMe,
sharedGroupNames, sharedGroupNames,
unblurredAvatarPath, unblurredAvatarUrl,
}), }),
...ariaProps ...ariaProps
}: Props): JSX.Element { }: Props): JSX.Element {
@ -139,15 +139,15 @@ export function Avatar({
useEffect(() => { useEffect(() => {
setImageBroken(false); setImageBroken(false);
}, [avatarPath]); }, [avatarUrl]);
useEffect(() => { useEffect(() => {
if (!avatarPath) { if (!avatarUrl) {
return noop; return noop;
} }
const image = new Image(); const image = new Image();
image.src = avatarPath; image.src = avatarUrl;
image.onerror = () => { image.onerror = () => {
log.warn('Avatar: Image failed to load; failing over to placeholder'); log.warn('Avatar: Image failed to load; failing over to placeholder');
setImageBroken(true); setImageBroken(true);
@ -156,10 +156,10 @@ export function Avatar({
return () => { return () => {
image.onerror = noop; image.onerror = noop;
}; };
}, [avatarPath]); }, [avatarUrl]);
const initials = getInitials(title); const initials = getInitials(title);
const hasImage = !noteToSelf && avatarPath && !imageBroken; const hasImage = !noteToSelf && avatarUrl && !imageBroken;
const shouldUseInitials = const shouldUseInitials =
!hasImage && !hasImage &&
conversationType === 'direct' && conversationType === 'direct' &&
@ -179,7 +179,7 @@ export function Avatar({
</div> </div>
); );
} else if (hasImage) { } else if (hasImage) {
assertDev(avatarPath, 'avatarPath should be defined here'); assertDev(avatarUrl, 'avatarUrl should be defined here');
assertDev( assertDev(
blur !== AvatarBlur.BlurPictureWithClickToView || blur !== AvatarBlur.BlurPictureWithClickToView ||
@ -195,7 +195,7 @@ export function Avatar({
<div <div
className="module-Avatar__image" className="module-Avatar__image"
style={{ style={{
backgroundImage: `url('${encodeURI(avatarPath)}')`, backgroundImage: `url('${avatarUrl}')`,
...(isBlurred ? { filter: `blur(${Math.ceil(size / 2)}px)` } : {}), ...(isBlurred ? { filter: `blur(${Math.ceil(size / 2)}px)` } : {}),
}} }}
/> />
@ -316,7 +316,7 @@ export function Avatar({
Boolean(storyRing) && 'module-Avatar--with-story', Boolean(storyRing) && 'module-Avatar--with-story',
storyRing === HasStories.Unread && 'module-Avatar--with-story--unread', storyRing === HasStories.Unread && 'module-Avatar--with-story--unread',
className, className,
avatarPath === SIGNAL_AVATAR_PATH avatarUrl === SIGNAL_AVATAR_PATH
? 'module-Avatar--signal-official' ? 'module-Avatar--signal-official'
: undefined : undefined
)} )}

View file

@ -18,7 +18,7 @@ const i18n = setupI18n('en', enMessages);
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
avatarColor: overrideProps.avatarColor || AvatarColors[9], avatarColor: overrideProps.avatarColor || AvatarColors[9],
avatarPath: overrideProps.avatarPath, avatarUrl: overrideProps.avatarUrl,
conversationId: '123', conversationId: '123',
conversationTitle: overrideProps.conversationTitle || 'Default Title', conversationTitle: overrideProps.conversationTitle || 'Default Title',
deleteAvatarFromDisk: action('deleteAvatarFromDisk'), deleteAvatarFromDisk: action('deleteAvatarFromDisk'),
@ -104,7 +104,7 @@ export function HasAvatar(): JSX.Element {
return ( return (
<AvatarEditor <AvatarEditor
{...createProps({ {...createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg', avatarUrl: '/fixtures/kitten-3-64-64.jpg',
})} })}
/> />
); );

View file

@ -24,7 +24,7 @@ import { missingCaseError } from '../util/missingCaseError';
export type PropsType = { export type PropsType = {
avatarColor?: AvatarColorType; avatarColor?: AvatarColorType;
avatarPath?: string; avatarUrl?: string;
avatarValue?: Uint8Array; avatarValue?: Uint8Array;
conversationId?: string; conversationId?: string;
conversationTitle?: string; conversationTitle?: string;
@ -46,7 +46,7 @@ enum EditMode {
export function AvatarEditor({ export function AvatarEditor({
avatarColor, avatarColor,
avatarPath, avatarUrl,
avatarValue, avatarValue,
conversationId, conversationId,
conversationTitle, conversationTitle,
@ -152,7 +152,7 @@ export function AvatarEditor({
}, []); }, []);
const hasChanges = const hasChanges =
initialAvatar !== avatarPreview || Boolean(pendingClear && avatarPath); initialAvatar !== avatarPreview || Boolean(pendingClear && avatarUrl);
let content: JSX.Element | undefined; let content: JSX.Element | undefined;
@ -162,7 +162,7 @@ export function AvatarEditor({
<div className="AvatarEditor__preview"> <div className="AvatarEditor__preview">
<AvatarPreview <AvatarPreview
avatarColor={avatarColor} avatarColor={avatarColor}
avatarPath={pendingClear ? undefined : avatarPath} avatarUrl={pendingClear ? undefined : avatarUrl}
avatarValue={avatarPreview} avatarValue={avatarPreview}
conversationTitle={conversationTitle} conversationTitle={conversationTitle}
i18n={i18n} i18n={i18n}

View file

@ -45,5 +45,5 @@ export function Person(args: PropsType): JSX.Element {
} }
export function Photo(args: PropsType): JSX.Element { export function Photo(args: PropsType): JSX.Element {
return <AvatarLightbox {...args} avatarPath="/fixtures/kitten-1-64-64.jpg" />; return <AvatarLightbox {...args} avatarUrl="/fixtures/kitten-1-64-64.jpg" />;
} }

View file

@ -11,7 +11,7 @@ import type { LocalizerType } from '../types/Util';
export type PropsType = { export type PropsType = {
avatarColor?: AvatarColorType; avatarColor?: AvatarColorType;
avatarPath?: string; avatarUrl?: string;
conversationTitle?: string; conversationTitle?: string;
i18n: LocalizerType; i18n: LocalizerType;
isGroup?: boolean; isGroup?: boolean;
@ -20,7 +20,7 @@ export type PropsType = {
export function AvatarLightbox({ export function AvatarLightbox({
avatarColor, avatarColor,
avatarPath, avatarUrl,
conversationTitle, conversationTitle,
i18n, i18n,
isGroup, isGroup,
@ -43,7 +43,7 @@ export function AvatarLightbox({
> >
<AvatarPreview <AvatarPreview
avatarColor={avatarColor} avatarColor={avatarColor}
avatarPath={avatarPath} avatarUrl={avatarUrl}
conversationTitle={conversationTitle} conversationTitle={conversationTitle}
i18n={i18n} i18n={i18n}
isGroup={isGroup} isGroup={isGroup}

View file

@ -24,7 +24,7 @@ const TEST_IMAGE = new Uint8Array(
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
avatarColor: overrideProps.avatarColor, avatarColor: overrideProps.avatarColor,
avatarPath: overrideProps.avatarPath, avatarUrl: overrideProps.avatarUrl,
avatarValue: overrideProps.avatarValue, avatarValue: overrideProps.avatarValue,
conversationTitle: overrideProps.conversationTitle, conversationTitle: overrideProps.conversationTitle,
i18n, i18n,
@ -81,7 +81,7 @@ export function Value(): JSX.Element {
export function Path(): JSX.Element { export function Path(): JSX.Element {
return ( return (
<AvatarPreview <AvatarPreview
{...createProps({ avatarPath: '/fixtures/kitten-3-64-64.jpg' })} {...createProps({ avatarUrl: '/fixtures/kitten-3-64-64.jpg' })}
/> />
); );
} }
@ -90,7 +90,7 @@ export function ValueAndPath(): JSX.Element {
return ( return (
<AvatarPreview <AvatarPreview
{...createProps({ {...createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg', avatarUrl: '/fixtures/kitten-3-64-64.jpg',
avatarValue: TEST_IMAGE, avatarValue: TEST_IMAGE,
})} })}
/> />

View file

@ -15,7 +15,7 @@ import { imagePathToBytes } from '../util/imagePathToBytes';
export type PropsType = { export type PropsType = {
avatarColor?: AvatarColorType; avatarColor?: AvatarColorType;
avatarPath?: string; avatarUrl?: string;
avatarValue?: Uint8Array; avatarValue?: Uint8Array;
conversationTitle?: string; conversationTitle?: string;
i18n: LocalizerType; i18n: LocalizerType;
@ -35,7 +35,7 @@ enum ImageStatus {
export function AvatarPreview({ export function AvatarPreview({
avatarColor = AvatarColors[0], avatarColor = AvatarColors[0],
avatarPath, avatarUrl,
avatarValue, avatarValue,
conversationTitle, conversationTitle,
i18n, i18n,
@ -48,15 +48,15 @@ export function AvatarPreview({
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const [avatarPreview, setAvatarPreview] = useState<Uint8Array | undefined>(); const [avatarPreview, setAvatarPreview] = useState<Uint8Array | undefined>();
// Loads the initial avatarPath if one is provided, but only if we're in editable mode. // Loads the initial avatarUrl if one is provided, but only if we're in editable mode.
// If we're not editable, we assume that we either have an avatarPath or we show a // If we're not editable, we assume that we either have an avatarUrl or we show a
// default avatar. // default avatar.
useEffect(() => { useEffect(() => {
if (!isEditable) { if (!isEditable) {
return; return;
} }
if (!avatarPath) { if (!avatarUrl) {
return noop; return noop;
} }
@ -64,7 +64,7 @@ export function AvatarPreview({
void (async () => { void (async () => {
try { try {
const buffer = await imagePathToBytes(avatarPath); const buffer = await imagePathToBytes(avatarUrl);
if (shouldCancel) { if (shouldCancel) {
return; return;
} }
@ -85,7 +85,7 @@ export function AvatarPreview({
return () => { return () => {
shouldCancel = true; shouldCancel = true;
}; };
}, [avatarPath, onAvatarLoaded, isEditable]); }, [avatarUrl, onAvatarLoaded, isEditable]);
// Ensures that when avatarValue changes we generate new URLs // Ensures that when avatarValue changes we generate new URLs
useEffect(() => { useEffect(() => {
@ -120,8 +120,8 @@ export function AvatarPreview({
} else if (objectUrl) { } else if (objectUrl) {
encodedPath = objectUrl; encodedPath = objectUrl;
imageStatus = ImageStatus.HasImage; imageStatus = ImageStatus.HasImage;
} else if (avatarPath) { } else if (avatarUrl) {
encodedPath = encodeURI(avatarPath); encodedPath = avatarUrl;
imageStatus = ImageStatus.HasImage; imageStatus = ImageStatus.HasImage;
} else { } else {
imageStatus = ImageStatus.Nothing; imageStatus = ImageStatus.Nothing;

View file

@ -5,13 +5,13 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
export type PropsType = { export type PropsType = {
avatarPath?: string; avatarUrl?: string;
children?: React.ReactNode; children?: React.ReactNode;
className?: string; className?: string;
}; };
export function CallBackgroundBlur({ export function CallBackgroundBlur({
avatarPath, avatarUrl,
children, children,
className, className,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
@ -19,15 +19,15 @@ export function CallBackgroundBlur({
<div <div
className={classNames( className={classNames(
'module-calling__background', 'module-calling__background',
!avatarPath && 'module-calling__background--no-avatar', !avatarUrl && 'module-calling__background--no-avatar',
className className
)} )}
> >
{avatarPath && ( {avatarUrl && (
<div <div
className="module-calling__background--blur" className="module-calling__background--blur"
style={{ style={{
backgroundImage: `url('${encodeURI(avatarPath)}')`, backgroundImage: `url('${avatarUrl}')`,
}} }}
/> />
)} )}

View file

@ -42,7 +42,7 @@ const i18n = setupI18n('en', enMessages);
const getConversation = () => const getConversation = () =>
getDefaultConversation({ getDefaultConversation({
id: '3051234567', id: '3051234567',
avatarPath: undefined, avatarUrl: undefined,
color: AvatarColors[0], color: AvatarColors[0],
title: 'Rick Sanchez', title: 'Rick Sanchez',
name: 'Rick Sanchez', name: 'Rick Sanchez',

View file

@ -13,7 +13,7 @@ export type Props = {
conversation: Pick< conversation: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'color' | 'color'
| 'isMe' | 'isMe'
| 'name' | 'name'
@ -21,7 +21,7 @@ export type Props = {
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
>; >;
i18n: LocalizerType; i18n: LocalizerType;
close: () => void; close: () => void;
@ -46,7 +46,7 @@ export function CallNeedPermissionScreen({
<div className="module-call-need-permission-screen"> <div className="module-call-need-permission-screen">
<Avatar <Avatar
acceptedMessageRequest={conversation.acceptedMessageRequest} acceptedMessageRequest={conversation.acceptedMessageRequest}
avatarPath={conversation.avatarPath} avatarUrl={conversation.avatarUrl}
badge={undefined} badge={undefined}
color={conversation.color || AvatarColors[0]} color={conversation.color || AvatarColors[0]}
noteToSelf={false} noteToSelf={false}

View file

@ -42,7 +42,7 @@ const i18n = setupI18n('en', enMessages);
const conversation = getDefaultConversation({ const conversation = getDefaultConversation({
id: '3051234567', id: '3051234567',
avatarPath: undefined, avatarUrl: undefined,
color: AvatarColors[0], color: AvatarColors[0],
title: 'Rick Sanchez', title: 'Rick Sanchez',
name: 'Rick Sanchez', name: 'Rick Sanchez',

View file

@ -432,7 +432,7 @@ export function CallScreen({
{isSendingVideo ? ( {isSendingVideo ? (
<video ref={localVideoRef} autoPlay /> <video ref={localVideoRef} autoPlay />
) : ( ) : (
<CallBackgroundBlur avatarPath={me.avatarPath}> <CallBackgroundBlur avatarUrl={me.avatarUrl}>
<div className="module-calling__spacer module-calling__camera-is-off-spacer" /> <div className="module-calling__spacer module-calling__camera-is-off-spacer" />
<div className="module-calling__camera-is-off"> <div className="module-calling__camera-is-off">
{i18n('icu:calling__your-video-is-off')} {i18n('icu:calling__your-video-is-off')}
@ -453,10 +453,10 @@ export function CallScreen({
autoPlay autoPlay
/> />
) : ( ) : (
<CallBackgroundBlur avatarPath={me.avatarPath}> <CallBackgroundBlur avatarUrl={me.avatarUrl}>
<Avatar <Avatar
acceptedMessageRequest acceptedMessageRequest
avatarPath={me.avatarPath} avatarUrl={me.avatarUrl}
badge={undefined} badge={undefined}
color={me.color || AvatarColors[0]} color={me.color || AvatarColors[0]}
noteToSelf={false} noteToSelf={false}

View file

@ -33,7 +33,7 @@ function createParticipant(
sharingScreen: Boolean(participantProps.sharingScreen), sharingScreen: Boolean(participantProps.sharingScreen),
videoAspectRatio: 1.3, videoAspectRatio: 1.3,
...getDefaultConversationWithServiceId({ ...getDefaultConversationWithServiceId({
avatarPath: participantProps.avatarPath, avatarUrl: participantProps.avatarUrl,
color: sample(AvatarColors), color: sample(AvatarColors),
isBlocked: Boolean(participantProps.isBlocked), isBlocked: Boolean(participantProps.isBlocked),
name: participantProps.name, name: participantProps.name,

View file

@ -80,7 +80,7 @@ function UnknownContacts({
return ( return (
<Avatar <Avatar
acceptedMessageRequest={participant.acceptedMessageRequest} acceptedMessageRequest={participant.acceptedMessageRequest}
avatarPath={participant.avatarPath} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
className="CallingAdhocCallInfo__UnknownContactAvatar" className="CallingAdhocCallInfo__UnknownContactAvatar"
color={AvatarColors[colorIndex]} color={AvatarColors[colorIndex]}
@ -211,7 +211,7 @@ export function CallingAdhocCallInfo({
<div className="module-calling-participants-list__avatar-and-name"> <div className="module-calling-participants-list__avatar-and-name">
<Avatar <Avatar
acceptedMessageRequest={participant.acceptedMessageRequest} acceptedMessageRequest={participant.acceptedMessageRequest}
avatarPath={participant.avatarPath} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
color={participant.color} color={participant.color}
conversationType="direct" conversationType="direct"

View file

@ -129,7 +129,7 @@ export function NoCameraLocalAvatar(): JSX.Element {
const props = createProps({ const props = createProps({
availableCameras: [], availableCameras: [],
me: getDefaultConversation({ me: getDefaultConversation({
avatarPath: '/fixtures/kitten-4-112-112.jpg', avatarUrl: '/fixtures/kitten-4-112-112.jpg',
color: AvatarColors[0], color: AvatarColors[0],
id: generateUuid(), id: generateUuid(),
serviceId: generateAci(), serviceId: generateAci(),

View file

@ -37,7 +37,7 @@ export type PropsType = {
conversation: Pick< conversation: Pick<
CallingConversationType, CallingConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'color' | 'color'
| 'isMe' | 'isMe'
| 'memberships' | 'memberships'
@ -49,7 +49,7 @@ export type PropsType = {
| 'systemNickname' | 'systemNickname'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
>; >;
getIsSharingPhoneNumberWithEverybody: () => boolean; getIsSharingPhoneNumberWithEverybody: () => boolean;
groupMembers?: Array< groupMembers?: Array<
@ -66,7 +66,7 @@ export type PropsType = {
isConversationTooBigToRing: boolean; isConversationTooBigToRing: boolean;
isCallFull?: boolean; isCallFull?: boolean;
me: Readonly< me: Readonly<
Pick<ConversationType, 'avatarPath' | 'color' | 'id' | 'serviceId'> Pick<ConversationType, 'avatarUrl' | 'color' | 'id' | 'serviceId'>
>; >;
onCallCanceled: () => void; onCallCanceled: () => void;
onJoinCall: () => void; onJoinCall: () => void;
@ -285,7 +285,7 @@ export function CallingLobby({
) : ( ) : (
<CallBackgroundBlur <CallBackgroundBlur
className="module-CallingLobby__local-preview module-CallingLobby__local-preview--camera-is-off" className="module-CallingLobby__local-preview module-CallingLobby__local-preview--camera-is-off"
avatarPath={me.avatarPath} avatarUrl={me.avatarUrl}
/> />
)} )}

View file

@ -31,7 +31,7 @@ function createParticipant(
sharingScreen: Boolean(participantProps.sharingScreen), sharingScreen: Boolean(participantProps.sharingScreen),
videoAspectRatio: 1.3, videoAspectRatio: 1.3,
...getDefaultConversationWithServiceId({ ...getDefaultConversationWithServiceId({
avatarPath: participantProps.avatarPath, avatarUrl: participantProps.avatarUrl,
color: sample(AvatarColors), color: sample(AvatarColors),
isBlocked: Boolean(participantProps.isBlocked), isBlocked: Boolean(participantProps.isBlocked),
name: participantProps.name, name: participantProps.name,

View file

@ -129,7 +129,7 @@ export const CallingParticipantsList = React.memo(
acceptedMessageRequest={ acceptedMessageRequest={
participant.acceptedMessageRequest participant.acceptedMessageRequest
} }
avatarPath={participant.avatarPath} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
color={participant.color} color={participant.color}
conversationType="direct" conversationType="direct"

View file

@ -244,7 +244,7 @@ export function CallingPendingParticipants({
<div className="module-calling-participants-list__avatar-and-name"> <div className="module-calling-participants-list__avatar-and-name">
<Avatar <Avatar
acceptedMessageRequest={participant.acceptedMessageRequest} acceptedMessageRequest={participant.acceptedMessageRequest}
avatarPath={participant.avatarPath} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
color={participant.color} color={participant.color}
conversationType="direct" conversationType="direct"
@ -306,7 +306,7 @@ export function CallingPendingParticipants({
<div className="module-calling-participants-list__avatar-and-name"> <div className="module-calling-participants-list__avatar-and-name">
<Avatar <Avatar
acceptedMessageRequest={participant.acceptedMessageRequest} acceptedMessageRequest={participant.acceptedMessageRequest}
avatarPath={participant.avatarPath} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
color={participant.color} color={participant.color}
conversationType="direct" conversationType="direct"

View file

@ -26,7 +26,7 @@ const i18n = setupI18n('en', enMessages);
const conversation: ConversationType = getDefaultConversation({ const conversation: ConversationType = getDefaultConversation({
id: '3051234567', id: '3051234567',
avatarPath: undefined, avatarUrl: undefined,
color: AvatarColors[0], color: AvatarColors[0],
title: 'Rick Sanchez', title: 'Rick Sanchez',
name: 'Rick Sanchez', name: 'Rick Sanchez',
@ -98,7 +98,7 @@ export function ContactWithAvatarAndNoVideo(args: PropsType): JSX.Element {
...getDefaultCall({}), ...getDefaultCall({}),
conversation: { conversation: {
...conversation, ...conversation,
avatarPath: 'https://www.fillmurray.com/64/64', avatarUrl: 'https://www.fillmurray.com/64/64',
}, },
remoteParticipants: [ remoteParticipants: [
{ hasRemoteVideo: false, presenting: false, title: 'Julian' }, { hasRemoteVideo: false, presenting: false, title: 'Julian' },

View file

@ -40,7 +40,7 @@ function NoVideo({
}): JSX.Element { }): JSX.Element {
const { const {
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
color, color,
type: conversationType, type: conversationType,
isMe, isMe,
@ -52,11 +52,11 @@ function NoVideo({
return ( return (
<div className="module-calling-pip__video--remote"> <div className="module-calling-pip__video--remote">
<CallBackgroundBlur avatarPath={avatarPath}> <CallBackgroundBlur avatarUrl={avatarUrl}>
<div className="module-calling-pip__video--avatar"> <div className="module-calling-pip__video--avatar">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={color || AvatarColors[0]} color={color || AvatarColors[0]}
noteToSelf={false} noteToSelf={false}

View file

@ -20,7 +20,7 @@ export type PropsType = {
conversation: Pick< conversation: Pick<
CallingConversationType, CallingConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'color' | 'color'
| 'isMe' | 'isMe'
| 'phoneNumber' | 'phoneNumber'
@ -30,7 +30,7 @@ export type PropsType = {
| 'systemNickname' | 'systemNickname'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
>; >;
i18n: LocalizerType; i18n: LocalizerType;
me: Pick<ConversationType, 'id' | 'serviceId'>; me: Pick<ConversationType, 'id' | 'serviceId'>;
@ -186,7 +186,7 @@ export function CallingPreCallInfo({
return ( return (
<div className="module-CallingPreCallInfo"> <div className="module-CallingPreCallInfo">
<Avatar <Avatar
avatarPath={conversation.avatarPath} avatarUrl={conversation.avatarUrl}
badge={undefined} badge={undefined}
color={conversation.color} color={conversation.color}
acceptedMessageRequest={conversation.acceptedMessageRequest} acceptedMessageRequest={conversation.acceptedMessageRequest}
@ -198,7 +198,7 @@ export function CallingPreCallInfo({
sharedGroupNames={conversation.sharedGroupNames} sharedGroupNames={conversation.sharedGroupNames}
size={AvatarSize.NINETY_SIX} size={AvatarSize.NINETY_SIX}
title={conversation.title} title={conversation.title}
unblurredAvatarPath={conversation.unblurredAvatarPath} unblurredAvatarUrl={conversation.unblurredAvatarUrl}
i18n={i18n} i18n={i18n}
/> />
<div className="module-CallingPreCallInfo__title"> <div className="module-CallingPreCallInfo__title">

View file

@ -35,7 +35,7 @@ const i18n = setupI18n('en', enMessages);
const conversation = getDefaultConversationWithServiceId({ const conversation = getDefaultConversationWithServiceId({
id: '3051234567', id: '3051234567',
avatarPath: undefined, avatarUrl: undefined,
color: AvatarColors[0], color: AvatarColors[0],
title: 'Rick Sanchez', title: 'Rick Sanchez',
name: 'Rick Sanchez', name: 'Rick Sanchez',

View file

@ -101,7 +101,7 @@ export function CallingRaisedHandsList({
<div className="CallingRaisedHandsList__AvatarAndName module-calling-participants-list__avatar-and-name"> <div className="CallingRaisedHandsList__AvatarAndName module-calling-participants-list__avatar-and-name">
<Avatar <Avatar
acceptedMessageRequest={participant.acceptedMessageRequest} acceptedMessageRequest={participant.acceptedMessageRequest}
avatarPath={participant.avatarPath} avatarUrl={participant.avatarUrl}
badge={undefined} badge={undefined}
color={participant.color} color={participant.color}
conversationType="direct" conversationType="direct"

View file

@ -789,7 +789,7 @@ export function CallsList({
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest acceptedMessageRequest
avatarPath={conversation.avatarPath} avatarUrl={conversation.avatarUrl}
color={conversation.color} color={conversation.color}
conversationType={conversation.type} conversationType={conversation.type}
i18n={i18n} i18n={i18n}

View file

@ -220,7 +220,7 @@ export function CallsNewCall({
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest acceptedMessageRequest
avatarPath={item.conversation.avatarPath} avatarUrl={item.conversation.avatarUrl}
conversationType="group" conversationType="group"
i18n={i18n} i18n={i18n}
isMe={false} isMe={false}

View file

@ -15,7 +15,7 @@ export type PropsType = {
ConversationType, ConversationType,
| 'about' | 'about'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'color' | 'color'
| 'firstName' | 'firstName'
| 'id' | 'id'
@ -24,12 +24,12 @@ export type PropsType = {
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
>; >;
export function ContactPill({ export function ContactPill({
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
color, color,
firstName, firstName,
i18n, i18n,
@ -39,7 +39,7 @@ export function ContactPill({
profileName, profileName,
sharedGroupNames, sharedGroupNames,
title, title,
unblurredAvatarPath, unblurredAvatarUrl,
onClickRemove, onClickRemove,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const removeLabel = i18n('icu:ContactPill--remove'); const removeLabel = i18n('icu:ContactPill--remove');
@ -48,7 +48,7 @@ export function ContactPill({
<div className="module-ContactPill"> <div className="module-ContactPill">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={color} color={color}
noteToSelf={false} noteToSelf={false}
@ -60,7 +60,7 @@ export function ContactPill({
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.TWENTY} size={AvatarSize.TWENTY}
unblurredAvatarPath={unblurredAvatarPath} unblurredAvatarUrl={unblurredAvatarUrl}
/> />
<ContactName <ContactName
firstName={firstName} firstName={firstName}

View file

@ -38,7 +38,7 @@ const contactPillProps = (
): ContactPillPropsType => ({ ): ContactPillPropsType => ({
...(overrideProps ?? ...(overrideProps ??
getDefaultConversation({ getDefaultConversation({
avatarPath: gifUrl, avatarUrl: gifUrl,
firstName: 'John', firstName: 'John',
id: 'abc123', id: 'abc123',
isMe: false, isMe: false,

View file

@ -275,7 +275,7 @@ const createConversation = (
: true, : true,
badges: [], badges: [],
isMe: overrideProps.isMe ?? false, isMe: overrideProps.isMe ?? false,
avatarPath: overrideProps.avatarPath ?? '', avatarUrl: overrideProps.avatarUrl ?? '',
id: overrideProps.id || '', id: overrideProps.id || '',
isSelected: overrideProps.isSelected ?? false, isSelected: overrideProps.isSelected ?? false,
title: overrideProps.title ?? 'Some Person', title: overrideProps.title ?? 'Some Person',
@ -308,7 +308,7 @@ export const ConversationName = (): JSX.Element => renderConversation();
export const ConversationNameAndAvatar = (): JSX.Element => export const ConversationNameAndAvatar = (): JSX.Element =>
renderConversation({ renderConversation({
avatarPath: '/fixtures/kitten-1-64-64.jpg', avatarUrl: '/fixtures/kitten-1-64-64.jpg',
}); });
export const ConversationWithYourself = (): JSX.Element => export const ConversationWithYourself = (): JSX.Element =>

View file

@ -371,7 +371,7 @@ export function ConversationList({
case RowType.Conversation: { case RowType.Conversation: {
const itemProps = pick(row.conversation, [ const itemProps = pick(row.conversation, [
'acceptedMessageRequest', 'acceptedMessageRequest',
'avatarPath', 'avatarUrl',
'badges', 'badges',
'color', 'color',
'draftPreview', 'draftPreview',
@ -393,7 +393,7 @@ export function ConversationList({
'title', 'title',
'type', 'type',
'typingContactIdTimestamps', 'typingContactIdTimestamps',
'unblurredAvatarPath', 'unblurredAvatarUrl',
'unreadCount', 'unreadCount',
'unreadMentionsCount', 'unreadMentionsCount',
'serviceId', 'serviceId',

View file

@ -52,7 +52,7 @@ function renderAvatar(
i18n: LocalizerType, i18n: LocalizerType,
{ {
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
color, color,
isMe, isMe,
phoneNumber, phoneNumber,
@ -62,7 +62,7 @@ function renderAvatar(
}: Pick< }: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'color' | 'color'
| 'isMe' | 'isMe'
| 'phoneNumber' | 'phoneNumber'
@ -73,10 +73,10 @@ function renderAvatar(
): JSX.Element { ): JSX.Element {
return ( return (
<div className="module-ongoing-call__remote-video-disabled"> <div className="module-ongoing-call__remote-video-disabled">
<CallBackgroundBlur avatarPath={avatarPath}> <CallBackgroundBlur avatarUrl={avatarUrl}>
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={color || AvatarColors[0]} color={color || AvatarColors[0]}
noteToSelf={false} noteToSelf={false}

View file

@ -85,7 +85,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
const { const {
acceptedMessageRequest, acceptedMessageRequest,
addedTime, addedTime,
avatarPath, avatarUrl,
color, color,
demuxId, demuxId,
hasRemoteAudio, hasRemoteAudio,
@ -378,7 +378,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
noVideoNode = ( noVideoNode = (
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={color || AvatarColors[0]} color={color || AvatarColors[0]}
noteToSelf={false} noteToSelf={false}
@ -510,7 +510,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
)} )}
{noVideoNode && ( {noVideoNode && (
<CallBackgroundBlur <CallBackgroundBlur
avatarPath={avatarPath} avatarUrl={avatarUrl}
className="module-ongoing-call__group-call-remote-participant-background" className="module-ongoing-call__group-call-remote-participant-background"
> >
{noVideoNode} {noVideoNode}

View file

@ -110,7 +110,7 @@ function Contacts({
<li key={contact.id} className="module-GroupDialog__contacts__contact"> <li key={contact.id} className="module-GroupDialog__contacts__contact">
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest} acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarUrl={contact.avatarUrl}
badge={getPreferredBadge(contact.badges)} badge={getPreferredBadge(contact.badges)}
color={contact.color} color={contact.color}
conversationType={contact.type} conversationType={contact.type}
@ -118,7 +118,7 @@ function Contacts({
noteToSelf={contact.isMe} noteToSelf={contact.isMe}
theme={theme} theme={theme}
title={contact.title} title={contact.title}
unblurredAvatarPath={contact.unblurredAvatarPath} unblurredAvatarUrl={contact.unblurredAvatarUrl}
sharedGroupNames={contact.sharedGroupNames} sharedGroupNames={contact.sharedGroupNames}
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}
i18n={i18n} i18n={i18n}

View file

@ -70,7 +70,7 @@ export const GroupV2JoinDialog = React.memo(function GroupV2JoinDialogInner({
<div className="module-group-v2-join-dialog__avatar"> <div className="module-group-v2-join-dialog__avatar">
<Avatar <Avatar
acceptedMessageRequest={false} acceptedMessageRequest={false}
avatarPath={avatar ? avatar.url : undefined} avatarUrl={avatar ? avatar.url : undefined}
badge={undefined} badge={undefined}
blur={AvatarBlur.NoBlur} blur={AvatarBlur.NoBlur}
loading={avatar && !avatar.url} loading={avatar && !avatar.url}

View file

@ -25,7 +25,7 @@ const commonProps = {
}, },
conversation: getDefaultConversation({ conversation: getDefaultConversation({
id: '3051234567', id: '3051234567',
avatarPath: undefined, avatarUrl: undefined,
name: 'Rick Sanchez', name: 'Rick Sanchez',
phoneNumber: '3051234567', phoneNumber: '3051234567',
profileName: 'Rick Sanchez', profileName: 'Rick Sanchez',
@ -38,7 +38,7 @@ const commonProps = {
const directConversation = getDefaultConversation({ const directConversation = getDefaultConversation({
id: '3051234567', id: '3051234567',
avatarPath: undefined, avatarUrl: undefined,
name: 'Rick Sanchez', name: 'Rick Sanchez',
phoneNumber: '3051234567', phoneNumber: '3051234567',
profileName: 'Rick Sanchez', profileName: 'Rick Sanchez',
@ -46,7 +46,7 @@ const directConversation = getDefaultConversation({
}); });
const groupConversation = getDefaultConversation({ const groupConversation = getDefaultConversation({
avatarPath: undefined, avatarUrl: undefined,
name: 'Tahoe Trip', name: 'Tahoe Trip',
title: 'Tahoe Trip', title: 'Tahoe Trip',
type: 'group', type: 'group',

View file

@ -28,7 +28,7 @@ export type PropsType = {
conversation: Pick< conversation: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'color' | 'color'
| 'id' | 'id'
| 'isMe' | 'isMe'
@ -194,7 +194,7 @@ export function IncomingCallBar(props: PropsType): JSX.Element | null {
const { const {
id: conversationId, id: conversationId,
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
color, color,
isMe, isMe,
phoneNumber, phoneNumber,
@ -275,7 +275,7 @@ export function IncomingCallBar(props: PropsType): JSX.Element | null {
<div className="IncomingCallBar__conversation--avatar"> <div className="IncomingCallBar__conversation--avatar">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={color || AvatarColors[0]} color={color || AvatarColors[0]}
noteToSelf={false} noteToSelf={false}

View file

@ -136,7 +136,7 @@ export function LeftPaneSearchInput({
> >
<Avatar <Avatar
acceptedMessageRequest={searchConversation.acceptedMessageRequest} acceptedMessageRequest={searchConversation.acceptedMessageRequest}
avatarPath={searchConversation.avatarPath} avatarUrl={searchConversation.avatarUrl}
badge={undefined} badge={undefined}
color={searchConversation.color} color={searchConversation.color}
conversationType={searchConversation.type} conversationType={searchConversation.type}
@ -146,7 +146,7 @@ export function LeftPaneSearchInput({
sharedGroupNames={searchConversation.sharedGroupNames} sharedGroupNames={searchConversation.sharedGroupNames}
size={AvatarSize.TWENTY} size={AvatarSize.TWENTY}
title={searchConversation.title} title={searchConversation.title}
unblurredAvatarPath={searchConversation.unblurredAvatarPath} unblurredAvatarUrl={searchConversation.unblurredAvatarUrl}
/> />
<button <button
aria-label={i18n('icu:clearSearch')} aria-label={i18n('icu:clearSearch')}

View file

@ -311,7 +311,7 @@ export function ConversationHeader(): JSX.Element {
{...createProps({})} {...createProps({})}
getConversation={() => ({ getConversation={() => ({
acceptedMessageRequest: true, acceptedMessageRequest: true,
avatarPath: '/fixtures/kitten-1-64-64.jpg', avatarUrl: '/fixtures/kitten-1-64-64.jpg',
badges: [], badges: [],
id: '1234', id: '1234',
isMe: false, isMe: false,

View file

@ -806,7 +806,7 @@ function LightboxHeader({
<div className="Lightbox__header--avatar"> <div className="Lightbox__header--avatar">
<Avatar <Avatar
acceptedMessageRequest={conversation.acceptedMessageRequest} acceptedMessageRequest={conversation.acceptedMessageRequest}
avatarPath={conversation.avatarPath} avatarUrl={conversation.avatarUrl}
badge={undefined} badge={undefined}
color={conversation.color} color={conversation.color}
conversationType={conversation.type} conversationType={conversation.type}
@ -817,7 +817,7 @@ function LightboxHeader({
sharedGroupNames={conversation.sharedGroupNames} sharedGroupNames={conversation.sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
title={conversation.title} title={conversation.title}
unblurredAvatarPath={conversation.unblurredAvatarPath} unblurredAvatarUrl={conversation.unblurredAvatarUrl}
/> />
</div> </div>
<div className="Lightbox__header--content"> <div className="Lightbox__header--content">

View file

@ -50,7 +50,7 @@ export function MyStoryButton({
const { const {
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
color, color,
isMe, isMe,
profileName, profileName,
@ -70,7 +70,7 @@ export function MyStoryButton({
<div className="MyStories__avatar-container"> <div className="MyStories__avatar-container">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={getAvatarColor(color)} color={getAvatarColor(color)}
conversationType="direct" conversationType="direct"
@ -123,7 +123,7 @@ export function MyStoryButton({
> >
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={getAvatarColor(color)} color={getAvatarColor(color)}
conversationType="direct" conversationType="direct"

View file

@ -372,7 +372,7 @@ export function NavTabs({
<span className="NavTabs__ItemContent"> <span className="NavTabs__ItemContent">
<Avatar <Avatar
acceptedMessageRequest acceptedMessageRequest
avatarPath={me.avatarPath} avatarUrl={me.avatarUrl}
badge={badge} badge={badge}
className="module-main-header__avatar" className="module-main-header__avatar"
color={me.color} color={me.color}

View file

@ -48,7 +48,7 @@ export default {
args: { args: {
aboutEmoji: '', aboutEmoji: '',
aboutText: casual.sentence, aboutText: casual.sentence,
profileAvatarPath: undefined, profileAvatarUrl: undefined,
conversationId: generateUuid(), conversationId: generateUuid(),
color: getRandomColor(), color: getRandomColor(),
deleteAvatarFromDisk: action('deleteAvatarFromDisk'), deleteAvatarFromDisk: action('deleteAvatarFromDisk'),
@ -127,7 +127,7 @@ FullSet.args = {
aboutText: 'Live. Laugh. Love', aboutText: 'Live. Laugh. Love',
familyName: casual.last_name, familyName: casual.last_name,
firstName: casual.first_name, firstName: casual.first_name,
profileAvatarPath: '/fixtures/kitten-3-64-64.jpg', profileAvatarUrl: '/fixtures/kitten-3-64-64.jpg',
}; };
export const WithFullName = Template.bind({}); export const WithFullName = Template.bind({});

View file

@ -72,7 +72,7 @@ type PropsExternalType = {
export type PropsDataType = { export type PropsDataType = {
aboutEmoji?: string; aboutEmoji?: string;
aboutText?: string; aboutText?: string;
profileAvatarPath?: string; profileAvatarUrl?: string;
color?: AvatarColorType; color?: AvatarColorType;
conversationId: string; conversationId: string;
familyName?: string; familyName?: string;
@ -155,7 +155,7 @@ export function ProfileEditor({
onProfileChanged, onProfileChanged,
onSetSkinTone, onSetSkinTone,
openUsernameReservationModal, openUsernameReservationModal,
profileAvatarPath, profileAvatarUrl,
recentEmojis, recentEmojis,
renderEditUsernameModalBody, renderEditUsernameModalBody,
replaceAvatar, replaceAvatar,
@ -192,8 +192,7 @@ export function ProfileEditor({
aboutEmoji, aboutEmoji,
aboutText, aboutText,
}); });
const [startingAvatarPath, setStartingAvatarPath] = const [startingAvatarUrl, setStartingAvatarUrl] = useState(profileAvatarUrl);
useState(profileAvatarPath);
const [oldAvatarBuffer, setOldAvatarBuffer] = useState< const [oldAvatarBuffer, setOldAvatarBuffer] = useState<
Uint8Array | undefined Uint8Array | undefined
@ -239,7 +238,7 @@ export function ProfileEditor({
const handleAvatarChanged = useCallback( const handleAvatarChanged = useCallback(
(avatar: Uint8Array | undefined) => { (avatar: Uint8Array | undefined) => {
// Do not display stale avatar from disk anymore. // Do not display stale avatar from disk anymore.
setStartingAvatarPath(undefined); setStartingAvatarUrl(undefined);
setAvatarBuffer(avatar); setAvatarBuffer(avatar);
setEditState(EditState.None); setEditState(EditState.None);
@ -301,7 +300,7 @@ export function ProfileEditor({
content = ( content = (
<AvatarEditor <AvatarEditor
avatarColor={color || AvatarColors[0]} avatarColor={color || AvatarColors[0]}
avatarPath={startingAvatarPath} avatarUrl={startingAvatarUrl}
avatarValue={avatarBuffer} avatarValue={avatarBuffer}
conversationId={conversationId} conversationId={conversationId}
conversationTitle={getFullNameText()} conversationTitle={getFullNameText()}
@ -675,7 +674,7 @@ export function ProfileEditor({
<> <>
<AvatarPreview <AvatarPreview
avatarColor={color} avatarColor={color}
avatarPath={startingAvatarPath} avatarUrl={startingAvatarUrl}
avatarValue={avatarBuffer} avatarValue={avatarBuffer}
conversationTitle={getFullNameText()} conversationTitle={getFullNameText()}
i18n={i18n} i18n={i18n}

View file

@ -40,7 +40,7 @@ export function ProfileEditorModal({
myProfileChanged, myProfileChanged,
onSetSkinTone, onSetSkinTone,
openUsernameReservationModal, openUsernameReservationModal,
profileAvatarPath, profileAvatarUrl,
recentEmojis, recentEmojis,
renderEditUsernameModalBody, renderEditUsernameModalBody,
replaceAvatar, replaceAvatar,
@ -117,7 +117,7 @@ export function ProfileEditorModal({
onProfileChanged={myProfileChanged} onProfileChanged={myProfileChanged}
onSetSkinTone={onSetSkinTone} onSetSkinTone={onSetSkinTone}
openUsernameReservationModal={openUsernameReservationModal} openUsernameReservationModal={openUsernameReservationModal}
profileAvatarPath={profileAvatarPath} profileAvatarUrl={profileAvatarUrl}
recentEmojis={recentEmojis} recentEmojis={recentEmojis}
renderEditUsernameModalBody={renderEditUsernameModalBody} renderEditUsernameModalBody={renderEditUsernameModalBody}
replaceAvatar={replaceAvatar} replaceAvatar={replaceAvatar}

View file

@ -18,7 +18,7 @@ const i18n = setupI18n('en', enMessages);
const contactWithAllData = getDefaultConversation({ const contactWithAllData = getDefaultConversation({
id: 'abc', id: 'abc',
avatarPath: undefined, avatarUrl: undefined,
profileName: '-*Smartest Dude*-', profileName: '-*Smartest Dude*-',
title: 'Rick Sanchez', title: 'Rick Sanchez',
name: 'Rick Sanchez', name: 'Rick Sanchez',
@ -27,7 +27,7 @@ const contactWithAllData = getDefaultConversation({
const contactWithJustProfileVerified = getDefaultConversation({ const contactWithJustProfileVerified = getDefaultConversation({
id: 'def', id: 'def',
avatarPath: undefined, avatarUrl: undefined,
title: '-*Smartest Dude*-', title: '-*Smartest Dude*-',
profileName: '-*Smartest Dude*-', profileName: '-*Smartest Dude*-',
name: undefined, name: undefined,
@ -37,7 +37,7 @@ const contactWithJustProfileVerified = getDefaultConversation({
const contactWithJustNumberVerified = getDefaultConversation({ const contactWithJustNumberVerified = getDefaultConversation({
id: 'xyz', id: 'xyz',
avatarPath: undefined, avatarUrl: undefined,
profileName: undefined, profileName: undefined,
name: undefined, name: undefined,
title: '(305) 123-4567', title: '(305) 123-4567',
@ -47,7 +47,7 @@ const contactWithJustNumberVerified = getDefaultConversation({
const contactWithNothing = getDefaultConversation({ const contactWithNothing = getDefaultConversation({
id: 'some-guid', id: 'some-guid',
avatarPath: undefined, avatarUrl: undefined,
profileName: undefined, profileName: undefined,
name: undefined, name: undefined,
phoneNumber: undefined, phoneNumber: undefined,

View file

@ -452,7 +452,7 @@ function ContactRow({
<li className="module-SafetyNumberChangeDialog__row" key={contact.id}> <li className="module-SafetyNumberChangeDialog__row" key={contact.id}>
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest} acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarUrl={contact.avatarUrl}
badge={getPreferredBadge(contact.badges)} badge={getPreferredBadge(contact.badges)}
color={contact.color} color={contact.color}
conversationType="direct" conversationType="direct"
@ -464,7 +464,7 @@ function ContactRow({
title={contact.title} title={contact.title}
sharedGroupNames={contact.sharedGroupNames} sharedGroupNames={contact.sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
unblurredAvatarPath={contact.unblurredAvatarPath} unblurredAvatarUrl={contact.unblurredAvatarUrl}
/> />
<div className="module-SafetyNumberChangeDialog__row--wrapper"> <div className="module-SafetyNumberChangeDialog__row--wrapper">
<div className="module-SafetyNumberChangeDialog__row--name"> <div className="module-SafetyNumberChangeDialog__row--name">

View file

@ -40,7 +40,7 @@ const contactWithAllData = getDefaultConversation({
}); });
const contactWithJustProfile = getDefaultConversation({ const contactWithJustProfile = getDefaultConversation({
avatarPath: undefined, avatarUrl: undefined,
title: '-*Smartest Dude*-', title: '-*Smartest Dude*-',
profileName: '-*Smartest Dude*-', profileName: '-*Smartest Dude*-',
name: undefined, name: undefined,
@ -48,7 +48,7 @@ const contactWithJustProfile = getDefaultConversation({
}); });
const contactWithJustNumber = getDefaultConversation({ const contactWithJustNumber = getDefaultConversation({
avatarPath: undefined, avatarUrl: undefined,
profileName: undefined, profileName: undefined,
name: undefined, name: undefined,
title: '(305) 123-4567', title: '(305) 123-4567',
@ -57,7 +57,7 @@ const contactWithJustNumber = getDefaultConversation({
const contactWithNothing = getDefaultConversation({ const contactWithNothing = getDefaultConversation({
id: 'some-guid', id: 'some-guid',
avatarPath: undefined, avatarUrl: undefined,
profileName: undefined, profileName: undefined,
title: 'Unknown contact', title: 'Unknown contact',
name: undefined, name: undefined,

View file

@ -558,7 +558,7 @@ export function SendStoryModal({
> >
<Avatar <Avatar
acceptedMessageRequest={group.acceptedMessageRequest} acceptedMessageRequest={group.acceptedMessageRequest}
avatarPath={group.avatarPath} avatarUrl={group.avatarUrl}
badge={undefined} badge={undefined}
color={group.color} color={group.color}
conversationType={group.type} conversationType={group.type}
@ -708,7 +708,7 @@ export function SendStoryModal({
{list.id === MY_STORY_ID ? ( {list.id === MY_STORY_ID ? (
<Avatar <Avatar
acceptedMessageRequest={me.acceptedMessageRequest} acceptedMessageRequest={me.acceptedMessageRequest}
avatarPath={me.avatarPath} avatarUrl={me.avatarUrl}
badge={undefined} badge={undefined}
color={me.color} color={me.color}
conversationType={me.type} conversationType={me.type}
@ -823,7 +823,7 @@ export function SendStoryModal({
> >
<Avatar <Avatar
acceptedMessageRequest={group.acceptedMessageRequest} acceptedMessageRequest={group.acceptedMessageRequest}
avatarPath={group.avatarPath} avatarUrl={group.avatarUrl}
badge={undefined} badge={undefined}
color={group.color} color={group.color}
conversationType={group.type} conversationType={group.type}

View file

@ -161,7 +161,7 @@ function DistributionListItem({
{isMyStory ? ( {isMyStory ? (
<Avatar <Avatar
acceptedMessageRequest={me.acceptedMessageRequest} acceptedMessageRequest={me.acceptedMessageRequest}
avatarPath={me.avatarPath} avatarUrl={me.avatarUrl}
badge={undefined} badge={undefined}
color={me.color} color={me.color}
conversationType={me.type} conversationType={me.type}
@ -215,7 +215,7 @@ function GroupStoryItem({
<span className="StoriesSettingsModal__list__left"> <span className="StoriesSettingsModal__list__left">
<Avatar <Avatar
acceptedMessageRequest={groupStory.acceptedMessageRequest} acceptedMessageRequest={groupStory.acceptedMessageRequest}
avatarPath={groupStory.avatarPath} avatarUrl={groupStory.avatarUrl}
badge={undefined} badge={undefined}
color={groupStory.color} color={groupStory.color}
conversationType={groupStory.type} conversationType={groupStory.type}
@ -676,7 +676,7 @@ export function DistributionListSettingsModal({
<span className="StoriesSettingsModal__list__left"> <span className="StoriesSettingsModal__list__left">
<Avatar <Avatar
acceptedMessageRequest={member.acceptedMessageRequest} acceptedMessageRequest={member.acceptedMessageRequest}
avatarPath={member.avatarPath} avatarUrl={member.avatarUrl}
badge={getPreferredBadge(member.badges)} badge={getPreferredBadge(member.badges)}
color={member.color} color={member.color}
conversationType={member.type} conversationType={member.type}
@ -1095,7 +1095,7 @@ export function EditDistributionListModal({
<span className="StoriesSettingsModal__list__left"> <span className="StoriesSettingsModal__list__left">
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest} acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarUrl={contact.avatarUrl}
badge={getPreferredBadge(contact.badges)} badge={getPreferredBadge(contact.badges)}
color={contact.color} color={contact.color}
conversationType={contact.type} conversationType={contact.type}
@ -1191,7 +1191,7 @@ export function EditDistributionListModal({
<ContactPill <ContactPill
key={contact.id} key={contact.id}
acceptedMessageRequest={contact.acceptedMessageRequest} acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarUrl={contact.avatarUrl}
color={contact.color} color={contact.color}
firstName={contact.firstName} firstName={contact.firstName}
i18n={i18n} i18n={i18n}
@ -1286,7 +1286,7 @@ export function GroupStorySettingsModal({
<div className="GroupStorySettingsModal__header"> <div className="GroupStorySettingsModal__header">
<Avatar <Avatar
acceptedMessageRequest={group.acceptedMessageRequest} acceptedMessageRequest={group.acceptedMessageRequest}
avatarPath={group.avatarPath} avatarUrl={group.avatarUrl}
badge={undefined} badge={undefined}
color={group.color} color={group.color}
conversationType={group.type} conversationType={group.type}
@ -1315,7 +1315,7 @@ export function GroupStorySettingsModal({
> >
<Avatar <Avatar
acceptedMessageRequest={member.acceptedMessageRequest} acceptedMessageRequest={member.acceptedMessageRequest}
avatarPath={member.avatarPath} avatarUrl={member.avatarUrl}
badge={undefined} badge={undefined}
color={member.color} color={member.color}
conversationType={member.type} conversationType={member.type}

View file

@ -129,7 +129,7 @@ export function StoryDetailsModal({
<div key={contact.id} className="StoryDetailsModal__contact"> <div key={contact.id} className="StoryDetailsModal__contact">
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest} acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarUrl={contact.avatarUrl}
badge={getPreferredBadge(contact.badges)} badge={getPreferredBadge(contact.badges)}
color={contact.color} color={contact.color}
conversationType="direct" conversationType="direct"
@ -141,7 +141,7 @@ export function StoryDetailsModal({
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
theme={ThemeType.dark} theme={ThemeType.dark}
title={contact.title} title={contact.title}
unblurredAvatarPath={contact.unblurredAvatarPath} unblurredAvatarUrl={contact.unblurredAvatarUrl}
/> />
<div className="StoryDetailsModal__contact__text"> <div className="StoryDetailsModal__contact__text">
<ContactName title={contact.title} /> <ContactName title={contact.title} />
@ -172,7 +172,7 @@ export function StoryDetailsModal({
<div className="StoryDetailsModal__contact"> <div className="StoryDetailsModal__contact">
<Avatar <Avatar
acceptedMessageRequest={sender.acceptedMessageRequest} acceptedMessageRequest={sender.acceptedMessageRequest}
avatarPath={sender.avatarPath} avatarUrl={sender.avatarUrl}
badge={getPreferredBadge(sender.badges)} badge={getPreferredBadge(sender.badges)}
color={sender.color} color={sender.color}
conversationType="direct" conversationType="direct"

View file

@ -35,7 +35,7 @@ export type PropsType = Pick<ConversationStoryType, 'group' | 'isHidden'> & {
function StoryListItemAvatar({ function StoryListItemAvatar({
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
avatarStoryRing, avatarStoryRing,
badges, badges,
color, color,
@ -49,7 +49,7 @@ function StoryListItemAvatar({
}: Pick< }: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'color' | 'color'
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
@ -65,7 +65,7 @@ function StoryListItemAvatar({
return ( return (
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={badges ? getPreferredBadge(badges) : undefined} badge={badges ? getPreferredBadge(badges) : undefined}
color={getAvatarColor(color)} color={getAvatarColor(color)}
conversationType="direct" conversationType="direct"

View file

@ -80,7 +80,7 @@ export function InAGroup(args: PropsType): JSX.Element {
<StoryViewer <StoryViewer
{...args} {...args}
group={getDefaultConversation({ group={getDefaultConversation({
avatarPath: '/fixtures/kitten-4-112-112.jpg', avatarUrl: '/fixtures/kitten-4-112-112.jpg',
title: 'Family Group', title: 'Family Group',
type: 'group', type: 'group',
})} })}

View file

@ -74,7 +74,7 @@ export type PropsType = {
group?: Pick< group?: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'color' | 'color'
| 'id' | 'id'
| 'name' | 'name'
@ -204,7 +204,7 @@ export function StoryViewer({
} = story; } = story;
const { const {
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
color, color,
isMe, isMe,
firstName, firstName,
@ -783,7 +783,7 @@ export function StoryViewer({
<div className="StoryViewer__meta__playback-bar__container"> <div className="StoryViewer__meta__playback-bar__container">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
color={getAvatarColor(color)} color={getAvatarColor(color)}
conversationType="direct" conversationType="direct"
@ -797,7 +797,7 @@ export function StoryViewer({
{group && ( {group && (
<Avatar <Avatar
acceptedMessageRequest={group.acceptedMessageRequest} acceptedMessageRequest={group.acceptedMessageRequest}
avatarPath={group.avatarPath} avatarUrl={group.avatarUrl}
badge={undefined} badge={undefined}
className="StoryViewer__meta--group-avatar" className="StoryViewer__meta--group-avatar"
color={getAvatarColor(group.color)} color={getAvatarColor(group.color)}

View file

@ -368,7 +368,7 @@ export function StoryViewsNRepliesModal({
<div> <div>
<Avatar <Avatar
acceptedMessageRequest={view.recipient.acceptedMessageRequest} acceptedMessageRequest={view.recipient.acceptedMessageRequest}
avatarPath={view.recipient.avatarPath} avatarUrl={view.recipient.avatarUrl}
badge={undefined} badge={undefined}
color={getAvatarColor(view.recipient.color)} color={getAvatarColor(view.recipient.color)}
conversationType="direct" conversationType="direct"
@ -550,7 +550,7 @@ function ReplyOrReactionMessage({
<div className="StoryViewsNRepliesModal__reaction--container"> <div className="StoryViewsNRepliesModal__reaction--container">
<Avatar <Avatar
acceptedMessageRequest={reply.author.acceptedMessageRequest} acceptedMessageRequest={reply.author.acceptedMessageRequest}
avatarPath={reply.author.avatarPath} avatarUrl={reply.author.avatarUrl}
badge={getPreferredBadge(reply.author.badges)} badge={getPreferredBadge(reply.author.badges)}
color={getAvatarColor(reply.author.color)} color={getAvatarColor(reply.author.color)}
conversationType="direct" conversationType="direct"

View file

@ -120,7 +120,7 @@ export function AboutContactModal({
<div className="AboutContactModal__row AboutContactModal__row--centered"> <div className="AboutContactModal__row AboutContactModal__row--centered">
<Avatar <Avatar
acceptedMessageRequest={conversation.acceptedMessageRequest} acceptedMessageRequest={conversation.acceptedMessageRequest}
avatarPath={conversation.avatarPath} avatarUrl={conversation.avatarUrl}
blur={avatarBlur} blur={avatarBlur}
onClick={avatarBlur === AvatarBlur.NoBlur ? undefined : onAvatarClick} onClick={avatarBlur === AvatarBlur.NoBlur ? undefined : onAvatarClick}
badge={undefined} badge={undefined}
@ -132,7 +132,7 @@ export function AboutContactModal({
sharedGroupNames={[]} sharedGroupNames={[]}
size={AvatarSize.TWO_HUNDRED_SIXTEEN} size={AvatarSize.TWO_HUNDRED_SIXTEEN}
title={conversation.title} title={conversation.title}
unblurredAvatarPath={conversation.unblurredAvatarPath} unblurredAvatarUrl={conversation.unblurredAvatarUrl}
/> />
</div> </div>

View file

@ -126,7 +126,7 @@ export function AttachmentList<T extends AttachmentType | AttachmentDraftType>({
if (isImage && canEditImages) { if (isImage && canEditImages) {
return ( return (
<div className="module-attachments--editable"> <div className="module-attachments--editable" key={key}>
{imgElement} {imgElement}
<div className="module-attachments__edit-icon" /> <div className="module-attachments__edit-icon" />
</div> </div>

View file

@ -314,7 +314,7 @@ export function ContactModal({
<div className="ContactModal"> <div className="ContactModal">
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest} acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarUrl={contact.avatarUrl}
badge={preferredBadge} badge={preferredBadge}
color={contact.color} color={contact.color}
conversationType="direct" conversationType="direct"
@ -338,7 +338,7 @@ export function ContactModal({
storyRing={hasStories} storyRing={hasStories}
theme={theme} theme={theme}
title={contact.title} title={contact.title}
unblurredAvatarPath={contact.unblurredAvatarPath} unblurredAvatarUrl={contact.unblurredAvatarUrl}
/> />
<button <button
type="button" type="button"
@ -472,7 +472,7 @@ export function ContactModal({
return ( return (
<AvatarLightbox <AvatarLightbox
avatarColor={contact.color} avatarColor={contact.color}
avatarPath={contact.avatarPath} avatarUrl={contact.avatarUrl}
conversationTitle={contact.title} conversationTitle={contact.title}
i18n={i18n} i18n={i18n}
onClose={() => setView(ContactModalView.Default)} onClose={() => setView(ContactModalView.Default)}

View file

@ -87,7 +87,7 @@ export function PrivateConvo(): JSX.Element {
conversation: getDefaultConversation({ conversation: getDefaultConversation({
color: getRandomColor(), color: getRandomColor(),
isVerified: true, isVerified: true,
avatarPath: gifUrl, avatarUrl: gifUrl,
title: 'Someone 🔥 Somewhere', title: 'Someone 🔥 Somewhere',
name: 'Someone 🔥 Somewhere', name: 'Someone 🔥 Somewhere',
phoneNumber: '(202) 555-0001', phoneNumber: '(202) 555-0001',

View file

@ -443,7 +443,7 @@ function HeaderContent({
<span className="module-ConversationHeader__header__avatar"> <span className="module-ConversationHeader__header__avatar">
<Avatar <Avatar
acceptedMessageRequest={conversation.acceptedMessageRequest} acceptedMessageRequest={conversation.acceptedMessageRequest}
avatarPath={conversation.avatarPath ?? undefined} avatarUrl={conversation.avatarUrl ?? undefined}
badge={badge ?? undefined} badge={badge ?? undefined}
color={conversation.color ?? undefined} color={conversation.color ?? undefined}
conversationType={conversation.type} conversationType={conversation.type}
@ -459,7 +459,7 @@ function HeaderContent({
storyRing={conversation.isMe ? undefined : hasStories ?? undefined} storyRing={conversation.isMe ? undefined : hasStories ?? undefined}
theme={theme} theme={theme}
title={conversation.title} title={conversation.title}
unblurredAvatarPath={conversation.unblurredAvatarPath ?? undefined} unblurredAvatarUrl={conversation.unblurredAvatarUrl ?? undefined}
/> />
</span> </span>
); );

View file

@ -84,7 +84,7 @@ DirectNoGroupsJustPhoneNumber.args = {
export const DirectNoGroupsNoData = Template.bind({}); export const DirectNoGroupsNoData = Template.bind({});
DirectNoGroupsNoData.args = { DirectNoGroupsNoData.args = {
avatarPath: undefined, avatarUrl: undefined,
phoneNumber: '', phoneNumber: '',
profileName: '', profileName: '',
title: casual.phone, title: casual.phone,
@ -93,7 +93,7 @@ DirectNoGroupsNoData.args = {
export const DirectNoGroupsNoDataNotAccepted = Template.bind({}); export const DirectNoGroupsNoDataNotAccepted = Template.bind({});
DirectNoGroupsNoDataNotAccepted.args = { DirectNoGroupsNoDataNotAccepted.args = {
acceptedMessageRequest: false, acceptedMessageRequest: false,
avatarPath: undefined, avatarUrl: undefined,
phoneNumber: '', phoneNumber: '',
profileName: '', profileName: '',
title: '', title: '',
@ -116,7 +116,7 @@ GroupManyMembers.args = {
export const GroupOneMember = Template.bind({}); export const GroupOneMember = Template.bind({});
GroupOneMember.args = { GroupOneMember.args = {
avatarPath: undefined, avatarUrl: undefined,
conversationType: 'group', conversationType: 'group',
groupDescription: casual.sentence, groupDescription: casual.sentence,
membersCount: 1, membersCount: 1,
@ -125,7 +125,7 @@ GroupOneMember.args = {
export const GroupZeroMembers = Template.bind({}); export const GroupZeroMembers = Template.bind({});
GroupZeroMembers.args = { GroupZeroMembers.args = {
avatarPath: undefined, avatarUrl: undefined,
conversationType: 'group', conversationType: 'group',
groupDescription: casual.sentence, groupDescription: casual.sentence,
membersCount: 0, membersCount: 0,

View file

@ -31,7 +31,7 @@ export type Props = {
phoneNumber?: string; phoneNumber?: string;
sharedGroupNames?: ReadonlyArray<string>; sharedGroupNames?: ReadonlyArray<string>;
unblurAvatar: (conversationId: string) => void; unblurAvatar: (conversationId: string) => void;
unblurredAvatarPath?: string; unblurredAvatarUrl?: string;
updateSharedGroups: (conversationId: string) => unknown; updateSharedGroups: (conversationId: string) => unknown;
theme: ThemeType; theme: ThemeType;
viewUserStories: ViewUserStoriesActionCreatorType; viewUserStories: ViewUserStoriesActionCreatorType;
@ -136,7 +136,7 @@ export function ConversationHero({
i18n, i18n,
about, about,
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
badge, badge,
color, color,
conversationType, conversationType,
@ -152,7 +152,7 @@ export function ConversationHero({
theme, theme,
title, title,
unblurAvatar, unblurAvatar,
unblurredAvatarPath, unblurredAvatarUrl,
updateSharedGroups, updateSharedGroups,
viewUserStories, viewUserStories,
toggleAboutContactModal, toggleAboutContactModal,
@ -174,10 +174,10 @@ export function ConversationHero({
if ( if (
shouldBlurAvatar({ shouldBlurAvatar({
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
isMe, isMe,
sharedGroupNames, sharedGroupNames,
unblurredAvatarPath, unblurredAvatarUrl,
}) })
) { ) {
avatarBlur = AvatarBlur.BlurPictureWithClickToView; avatarBlur = AvatarBlur.BlurPictureWithClickToView;
@ -221,7 +221,7 @@ export function ConversationHero({
<div className="module-conversation-hero"> <div className="module-conversation-hero">
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={badge} badge={badge}
blur={avatarBlur} blur={avatarBlur}
className="module-conversation-hero__avatar" className="module-conversation-hero__avatar"

View file

@ -229,7 +229,7 @@ export type PropsData = {
author: Pick< author: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'badges' | 'badges'
| 'color' | 'color'
| 'id' | 'id'
@ -238,7 +238,7 @@ export type PropsData = {
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
>; >;
conversationType: ConversationTypeType; conversationType: ConversationTypeType;
attachments?: ReadonlyArray<AttachmentType>; attachments?: ReadonlyArray<AttachmentType>;
@ -1814,7 +1814,7 @@ export class Message extends React.PureComponent<Props, State> {
) : ( ) : (
<Avatar <Avatar
acceptedMessageRequest={author.acceptedMessageRequest} acceptedMessageRequest={author.acceptedMessageRequest}
avatarPath={author.avatarPath} avatarUrl={author.avatarUrl}
badge={getPreferredBadge(author.badges)} badge={getPreferredBadge(author.badges)}
color={author.color} color={author.color}
conversationType="direct" conversationType="direct"
@ -1832,7 +1832,7 @@ export class Message extends React.PureComponent<Props, State> {
size={GROUP_AVATAR_SIZE} size={GROUP_AVATAR_SIZE}
theme={theme} theme={theme}
title={author.title} title={author.title}
unblurredAvatarPath={author.unblurredAvatarPath} unblurredAvatarUrl={author.unblurredAvatarUrl}
/> />
)} )}
</div> </div>

View file

@ -37,7 +37,7 @@ import {
export type Contact = Pick< export type Contact = Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'badges' | 'badges'
| 'color' | 'color'
| 'id' | 'id'
@ -46,7 +46,7 @@ export type Contact = Pick<
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
> & { > & {
status?: SendStatus; status?: SendStatus;
statusTimestamp?: number; statusTimestamp?: number;
@ -154,7 +154,7 @@ export function MessageDetail({
function renderAvatar(contact: Contact): JSX.Element { function renderAvatar(contact: Contact): JSX.Element {
const { const {
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
badges, badges,
color, color,
isMe, isMe,
@ -162,13 +162,13 @@ export function MessageDetail({
profileName, profileName,
sharedGroupNames, sharedGroupNames,
title, title,
unblurredAvatarPath, unblurredAvatarUrl,
} = contact; } = contact;
return ( return (
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={getPreferredBadge(badges)} badge={getPreferredBadge(badges)}
color={color} color={color}
conversationType="direct" conversationType="direct"
@ -180,7 +180,7 @@ export function MessageDetail({
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
unblurredAvatarPath={unblurredAvatarPath} unblurredAvatarUrl={unblurredAvatarUrl}
/> />
); );
} }

View file

@ -614,9 +614,7 @@ function ThumbnailImage({
return ( return (
<div <div
className={className} className={className}
style={ style={loadedSrc ? { backgroundImage: `url('${loadedSrc}')` } : {}}
loadedSrc ? { backgroundImage: `url('${encodeURI(loadedSrc)}')` } : {}
}
> >
{children} {children}
</div> </div>

View file

@ -22,7 +22,7 @@ export type Reaction = {
from: Pick< from: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'badges' | 'badges'
| 'color' | 'color'
| 'id' | 'id'
@ -226,7 +226,7 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
<div className="module-reaction-viewer__body__row__avatar"> <div className="module-reaction-viewer__body__row__avatar">
<Avatar <Avatar
acceptedMessageRequest={from.acceptedMessageRequest} acceptedMessageRequest={from.acceptedMessageRequest}
avatarPath={from.avatarPath} avatarUrl={from.avatarUrl}
badge={getPreferredBadge(from.badges)} badge={getPreferredBadge(from.badges)}
conversationType="direct" conversationType="direct"
sharedGroupNames={from.sharedGroupNames} sharedGroupNames={from.sharedGroupNames}

View file

@ -399,7 +399,7 @@ const renderHeroRow = () => {
<ConversationHero <ConversationHero
about={getAbout()} about={getAbout()}
acceptedMessageRequest acceptedMessageRequest
avatarPath={getAvatarPath()} avatarUrl={getAvatarPath()}
badge={undefined} badge={undefined}
conversationType="direct" conversationType="direct"
id={getDefaultConversation().id} id={getDefaultConversation().id}

View file

@ -721,7 +721,7 @@ ReactionsShortMessage.args = {
export const AvatarInGroup = Template.bind({}); export const AvatarInGroup = Template.bind({});
AvatarInGroup.args = { AvatarInGroup.args = {
author: getDefaultConversation({ avatarPath: pngUrl }), author: getDefaultConversation({ avatarUrl: pngUrl }),
conversationType: 'group', conversationType: 'group',
status: 'sent', status: 'sent',
text: 'Hello it is me, the saxophone.', text: 'Hello it is me, the saxophone.',

View file

@ -27,7 +27,7 @@ const CONTACTS = times(10, index => {
return getDefaultConversation({ return getDefaultConversation({
id: `contact-${index}`, id: `contact-${index}`,
acceptedMessageRequest: false, acceptedMessageRequest: false,
avatarPath: '', avatarUrl: '',
badges: [], badges: [],
color: AvatarColors[index], color: AvatarColors[index],
name: `${letter} ${letter}`, name: `${letter} ${letter}`,

View file

@ -19,7 +19,7 @@ const MAX_AVATARS_COUNT = 3;
type TypingContactType = Pick< type TypingContactType = Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'badges' | 'badges'
| 'color' | 'color'
| 'id' | 'id'
@ -120,7 +120,7 @@ function TypingBubbleAvatar({
<animated.div className="module-message__typing-avatar" style={springProps}> <animated.div className="module-message__typing-avatar" style={springProps}>
<Avatar <Avatar
acceptedMessageRequest={contact.acceptedMessageRequest} acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarUrl={contact.avatarUrl}
badge={getPreferredBadge(contact.badges)} badge={getPreferredBadge(contact.badges)}
color={contact.color} color={contact.color}
conversationType="direct" conversationType="direct"

View file

@ -25,7 +25,7 @@ export function renderAvatar({
}): JSX.Element { }): JSX.Element {
const { avatar } = contact; const { avatar } = contact;
const avatarPath = avatar && avatar.avatar && avatar.avatar.path; const avatarUrl = avatar && avatar.avatar && avatar.avatar.path;
const pending = avatar && avatar.avatar && avatar.avatar.pending; const pending = avatar && avatar.avatar && avatar.avatar.pending;
const title = getName(contact) || ''; const title = getName(contact) || '';
const spinnerSvgSize = size < 50 ? 'small' : 'normal'; const spinnerSvgSize = size < 50 ? 'small' : 'normal';
@ -46,7 +46,7 @@ export function renderAvatar({
return ( return (
<Avatar <Avatar
acceptedMessageRequest={false} acceptedMessageRequest={false}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={undefined} badge={undefined}
blur={AvatarBlur.NoBlur} blur={AvatarBlur.NoBlur}
color={AvatarColors[0]} color={AvatarColors[0]}

View file

@ -412,7 +412,7 @@ export function ChooseGroupMembersModal({
<ContactPill <ContactPill
key={contact.id} key={contact.id}
acceptedMessageRequest={contact.acceptedMessageRequest} acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarUrl={contact.avatarUrl}
color={contact.color} color={contact.color}
firstName={contact.systemGivenName ?? contact.firstName} firstName={contact.systemGivenName ?? contact.firstName}
i18n={i18n} i18n={i18n}

View file

@ -245,7 +245,7 @@ export function ConversationDetails({
modalNode = ( modalNode = (
<EditConversationAttributesModal <EditConversationAttributesModal
avatarColor={conversation.color} avatarColor={conversation.color}
avatarPath={conversation.avatarPath} avatarUrl={conversation.avatarUrl}
conversationId={conversation.id} conversationId={conversation.id}
groupDescription={conversation.groupDescription} groupDescription={conversation.groupDescription}
i18n={i18n} i18n={i18n}

View file

@ -107,7 +107,7 @@ export function ConversationDetailsHeader({
modal = ( modal = (
<AvatarLightbox <AvatarLightbox
avatarColor={conversation.color} avatarColor={conversation.color}
avatarPath={conversation.avatarPath} avatarUrl={conversation.avatarUrl}
conversationTitle={conversation.title} conversationTitle={conversation.title}
i18n={i18n} i18n={i18n}
isGroup={isGroup} isGroup={isGroup}

View file

@ -20,7 +20,7 @@ export default {
type PropsType = ComponentProps<typeof EditConversationAttributesModal>; type PropsType = ComponentProps<typeof EditConversationAttributesModal>;
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
avatarPath: undefined, avatarUrl: undefined,
conversationId: '123', conversationId: '123',
i18n, i18n,
initiallyFocusDescription: false, initiallyFocusDescription: false,
@ -43,7 +43,7 @@ export function AvatarAndTitle(): JSX.Element {
return ( return (
<EditConversationAttributesModal <EditConversationAttributesModal
{...createProps({ {...createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg', avatarUrl: '/fixtures/kitten-3-64-64.jpg',
})} })}
/> />
); );

View file

@ -23,7 +23,7 @@ import type { AvatarColorType } from '../../../types/Colors';
type PropsType = { type PropsType = {
avatarColor?: AvatarColorType; avatarColor?: AvatarColorType;
avatarPath?: string; avatarUrl?: string;
conversationId: string; conversationId: string;
groupDescription?: string; groupDescription?: string;
i18n: LocalizerType; i18n: LocalizerType;
@ -46,7 +46,7 @@ type PropsType = {
export function EditConversationAttributesModal({ export function EditConversationAttributesModal({
avatarColor, avatarColor,
avatarPath: externalAvatarPath, avatarUrl: externalAvatarUrl,
conversationId, conversationId,
groupDescription: externalGroupDescription = '', groupDescription: externalGroupDescription = '',
i18n, i18n,
@ -66,7 +66,7 @@ export function EditConversationAttributesModal({
const focusDescription = focusDescriptionRef.current; const focusDescription = focusDescriptionRef.current;
const startingTitleRef = useRef<string>(externalTitle); const startingTitleRef = useRef<string>(externalTitle);
const startingAvatarPathRef = useRef<undefined | string>(externalAvatarPath); const startingAvatarUrlRef = useRef<undefined | string>(externalAvatarUrl);
const [editingAvatar, setEditingAvatar] = useState(false); const [editingAvatar, setEditingAvatar] = useState(false);
const [avatar, setAvatar] = useState<undefined | Uint8Array>(); const [avatar, setAvatar] = useState<undefined | Uint8Array>();
@ -87,7 +87,7 @@ export function EditConversationAttributesModal({
}; };
const hasChangedExternally = const hasChangedExternally =
startingAvatarPathRef.current !== externalAvatarPath || startingAvatarUrlRef.current !== externalAvatarUrl ||
startingTitleRef.current !== externalTitle; startingTitleRef.current !== externalTitle;
const hasTitleChanged = trimmedTitle !== externalTitle.trim(); const hasTitleChanged = trimmedTitle !== externalTitle.trim();
const hasGroupDescriptionChanged = const hasGroupDescriptionChanged =
@ -123,16 +123,14 @@ export function EditConversationAttributesModal({
makeRequest(request); makeRequest(request);
}; };
const avatarPathForPreview = hasAvatarChanged const avatarUrlForPreview = hasAvatarChanged ? undefined : externalAvatarUrl;
? undefined
: externalAvatarPath;
let content: JSX.Element; let content: JSX.Element;
if (editingAvatar) { if (editingAvatar) {
content = ( content = (
<AvatarEditor <AvatarEditor
avatarColor={avatarColor} avatarColor={avatarColor}
avatarPath={avatarPathForPreview} avatarUrl={avatarUrlForPreview}
avatarValue={avatar} avatarValue={avatar}
conversationId={conversationId} conversationId={conversationId}
deleteAvatarFromDisk={deleteAvatarFromDisk} deleteAvatarFromDisk={deleteAvatarFromDisk}
@ -161,7 +159,7 @@ export function EditConversationAttributesModal({
> >
<AvatarPreview <AvatarPreview
avatarColor={avatarColor} avatarColor={avatarColor}
avatarPath={avatarPathForPreview} avatarUrl={avatarUrlForPreview}
avatarValue={avatar} avatarValue={avatar}
i18n={i18n} i18n={i18n}
isEditable isEditable

View file

@ -58,7 +58,7 @@ type PropsType = {
} & Pick< } & Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'color' | 'color'
| 'groupId' | 'groupId'
| 'isMe' | 'isMe'
@ -67,7 +67,7 @@ type PropsType = {
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
| 'serviceId' | 'serviceId'
> & > &
( (
@ -79,7 +79,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
React.memo(function BaseConversationListItem(props) { React.memo(function BaseConversationListItem(props) {
const { const {
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
avatarSize, avatarSize,
buttonAriaLabel, buttonAriaLabel,
checked, checked,
@ -106,7 +106,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
shouldShowSpinner, shouldShowSpinner,
testId: overrideTestId, testId: overrideTestId,
title, title,
unblurredAvatarPath, unblurredAvatarUrl,
unreadCount, unreadCount,
unreadMentionsCount, unreadMentionsCount,
serviceId, serviceId,
@ -194,7 +194,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
<div className={AVATAR_CONTAINER_CLASS_NAME}> <div className={AVATAR_CONTAINER_CLASS_NAME}>
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
color={color} color={color}
conversationType={conversationType} conversationType={conversationType}
noteToSelf={isAvatarNoteToSelf} noteToSelf={isAvatarNoteToSelf}
@ -206,7 +206,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={avatarSize ?? AvatarSize.FORTY_EIGHT} size={avatarSize ?? AvatarSize.FORTY_EIGHT}
unblurredAvatarPath={unblurredAvatarPath} unblurredAvatarUrl={unblurredAvatarUrl}
// This is here to appease the type checker. // This is here to appease the type checker.
{...(props.badge {...(props.badge
? { badge: props.badge, theme: props.theme } ? { badge: props.badge, theme: props.theme }

View file

@ -27,7 +27,7 @@ export type PropsDataType = {
ConversationType, ConversationType,
| 'about' | 'about'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'color' | 'color'
| 'groupId' | 'groupId'
| 'id' | 'id'
@ -37,7 +37,7 @@ export type PropsDataType = {
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
| 'serviceId' | 'serviceId'
>; >;
@ -56,7 +56,7 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
function ContactCheckbox({ function ContactCheckbox({
about, about,
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
badge, badge,
color, color,
disabledReason, disabledReason,
@ -71,7 +71,7 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
theme, theme,
title, title,
type, type,
unblurredAvatarPath, unblurredAvatarUrl,
}) { }) {
const disabled = Boolean(disabledReason); const disabled = Boolean(disabledReason);
@ -104,7 +104,7 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
color={color} color={color}
conversationType={type} conversationType={type}
noteToSelf={Boolean(isMe)} noteToSelf={Boolean(isMe)}
@ -115,7 +115,7 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
unblurredAvatarPath={unblurredAvatarPath} unblurredAvatarUrl={unblurredAvatarUrl}
// appease the type checker. // appease the type checker.
{...(badge ? { badge, theme } : { badge: undefined })} {...(badge ? { badge, theme } : { badge: undefined })}
/> />

View file

@ -23,7 +23,7 @@ export type ContactListItemConversationType = Pick<
ConversationType, ConversationType,
| 'about' | 'about'
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'badges' | 'badges'
| 'color' | 'color'
| 'groupId' | 'groupId'
@ -37,7 +37,7 @@ export type ContactListItemConversationType = Pick<
| 'systemFamilyName' | 'systemFamilyName'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
| 'username' | 'username'
| 'e164' | 'e164'
| 'serviceId' | 'serviceId'
@ -64,7 +64,7 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
function ContactListItem({ function ContactListItem({
about, about,
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
badge, badge,
color, color,
hasContextMenu, hasContextMenu,
@ -85,7 +85,7 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
theme, theme,
title, title,
type, type,
unblurredAvatarPath, unblurredAvatarUrl,
serviceId, serviceId,
}) { }) {
const [isConfirmingBlocking, setConfirmingBlocking] = useState(false); const [isConfirmingBlocking, setConfirmingBlocking] = useState(false);
@ -265,7 +265,7 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
color={color} color={color}
conversationType={type} conversationType={type}
noteToSelf={Boolean(isMe)} noteToSelf={Boolean(isMe)}
@ -276,7 +276,7 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
title={title} title={title}
sharedGroupNames={sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO} size={AvatarSize.THIRTY_TWO}
unblurredAvatarPath={unblurredAvatarPath} unblurredAvatarUrl={unblurredAvatarUrl}
// This is here to appease the type checker. // This is here to appease the type checker.
{...(badge ? { badge, theme } : { badge: undefined })} {...(badge ? { badge, theme } : { badge: undefined })}
/> />

View file

@ -39,7 +39,7 @@ export type MessageStatusType = typeof MessageStatuses[number];
export type PropsData = Pick< export type PropsData = Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'badges' | 'badges'
| 'color' | 'color'
| 'draftPreview' | 'draftPreview'
@ -62,7 +62,7 @@ export type PropsData = Pick<
| 'title' | 'title'
| 'type' | 'type'
| 'typingContactIdTimestamps' | 'typingContactIdTimestamps'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
| 'unreadCount' | 'unreadCount'
| 'unreadMentionsCount' | 'unreadMentionsCount'
| 'serviceId' | 'serviceId'
@ -82,7 +82,7 @@ export type Props = PropsData & PropsHousekeeping;
export const ConversationListItem: FunctionComponent<Props> = React.memo( export const ConversationListItem: FunctionComponent<Props> = React.memo(
function ConversationListItem({ function ConversationListItem({
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarUrl,
badge, badge,
buttonAriaLabel, buttonAriaLabel,
color, color,
@ -107,7 +107,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
title, title,
type, type,
typingContactIdTimestamps, typingContactIdTimestamps,
unblurredAvatarPath, unblurredAvatarUrl,
unreadCount, unreadCount,
unreadMentionsCount, unreadMentionsCount,
serviceId, serviceId,
@ -206,7 +206,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
return ( return (
<BaseConversationListItem <BaseConversationListItem
acceptedMessageRequest={acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath} avatarUrl={avatarUrl}
badge={badge} badge={badge}
buttonAriaLabel={buttonAriaLabel} buttonAriaLabel={buttonAriaLabel}
color={color} color={color}
@ -230,7 +230,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
title={title} title={title}
unreadCount={unreadCount} unreadCount={unreadCount}
unreadMentionsCount={unreadMentionsCount} unreadMentionsCount={unreadMentionsCount}
unblurredAvatarPath={unblurredAvatarPath} unblurredAvatarUrl={unblurredAvatarUrl}
serviceId={serviceId} serviceId={serviceId}
/> />
); );

View file

@ -16,7 +16,7 @@ export enum DisabledReason {
export type GroupListItemConversationType = Pick< export type GroupListItemConversationType = Pick<
ConversationType, ConversationType,
'id' | 'title' | 'avatarPath' 'id' | 'title' | 'avatarUrl'
> & { > & {
disabledReason: DisabledReason | undefined; disabledReason: DisabledReason | undefined;
membersCount: number; membersCount: number;
@ -56,7 +56,7 @@ export function GroupListItem({
leading={ leading={
<Avatar <Avatar
acceptedMessageRequest acceptedMessageRequest
avatarPath={group.avatarPath} avatarUrl={group.avatarUrl}
conversationType="group" conversationType="group"
i18n={i18n} i18n={i18n}
isMe={false} isMe={false}

View file

@ -39,7 +39,7 @@ export type PropsDataType = {
from: Pick< from: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarUrl'
| 'badges' | 'badges'
| 'color' | 'color'
| 'isMe' | 'isMe'
@ -48,7 +48,7 @@ export type PropsDataType = {
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
>; >;
to: Pick< to: Pick<
@ -184,7 +184,7 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
return ( return (
<BaseConversationListItem <BaseConversationListItem
acceptedMessageRequest={from.acceptedMessageRequest} acceptedMessageRequest={from.acceptedMessageRequest}
avatarPath={from.avatarPath} avatarUrl={from.avatarUrl}
badge={getPreferredBadge(from.badges)} badge={getPreferredBadge(from.badges)}
color={from.color} color={from.color}
conversationType="direct" conversationType="direct"
@ -202,7 +202,7 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
sharedGroupNames={from.sharedGroupNames} sharedGroupNames={from.sharedGroupNames}
theme={theme} theme={theme}
title={from.title} title={from.title}
unblurredAvatarPath={from.unblurredAvatarPath} unblurredAvatarUrl={from.unblurredAvatarUrl}
/> />
); );
} }

View file

@ -228,7 +228,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
<ContactPill <ContactPill
key={contact.id} key={contact.id}
acceptedMessageRequest={contact.acceptedMessageRequest} acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath} avatarUrl={contact.avatarUrl}
color={contact.color} color={contact.color}
firstName={contact.systemGivenName ?? contact.firstName} firstName={contact.systemGivenName ?? contact.firstName}
i18n={i18n} i18n={i18n}

View file

@ -438,25 +438,17 @@ export function parseGroupLink(value: string): {
// Group Modifications // Group Modifications
async function uploadAvatar( async function uploadAvatar(options: {
options: {
logId: string; logId: string;
publicParams: string; publicParams: string;
secretParams: string; secretParams: string;
} & ({ path: string } | { data: Uint8Array }) data: Uint8Array;
): Promise<UploadedAvatarType> { }): Promise<UploadedAvatarType> {
const { logId, publicParams, secretParams } = options; const { logId, publicParams, secretParams, data } = options;
try { try {
const clientZkGroupCipher = getClientZkGroupCipher(secretParams); const clientZkGroupCipher = getClientZkGroupCipher(secretParams);
let data: Uint8Array;
if ('data' in options) {
({ data } = options);
} else {
data = await window.Signal.Migrations.readAttachmentData(options.path);
}
const hash = computeHash(data); const hash = computeHash(data);
const blobPlaintext = Proto.GroupAttributeBlob.encode({ const blobPlaintext = Proto.GroupAttributeBlob.encode({
@ -1967,9 +1959,9 @@ export async function createGroupV2(
try { try {
avatarAttribute = { avatarAttribute = {
url: uploadedAvatar.key, url: uploadedAvatar.key,
path: await window.Signal.Migrations.writeNewAttachmentData( ...(await window.Signal.Migrations.writeNewAttachmentData(
uploadedAvatar.data uploadedAvatar.data
), )),
hash: uploadedAvatar.hash, hash: uploadedAvatar.hash,
}; };
} catch (err) { } catch (err) {
@ -2382,17 +2374,21 @@ export async function initiateMigrationToGroupV2(
// - name // - name
// - expireTimer // - expireTimer
let avatarAttribute: ConversationAttributesType['avatar']; let avatarAttribute: ConversationAttributesType['avatar'];
const avatarPath = conversation.attributes.avatar?.path;
if (avatarPath) { const { avatar: currentAvatar } = conversation.attributes;
if (currentAvatar?.path) {
const avatarData = await window.Signal.Migrations.readAttachmentData(
currentAvatar
);
const { hash, key } = await uploadAvatar({ const { hash, key } = await uploadAvatar({
logId, logId,
publicParams, publicParams,
secretParams, secretParams,
path: avatarPath, data: avatarData,
}); });
avatarAttribute = { avatarAttribute = {
...currentAvatar,
url: key, url: key,
path: avatarPath,
hash, hash,
}; };
} }
@ -5577,10 +5573,10 @@ export async function applyNewAvatar(
); );
} }
const path = await window.Signal.Migrations.writeNewAttachmentData(data); const local = await window.Signal.Migrations.writeNewAttachmentData(data);
result.avatar = { result.avatar = {
url: newAvatarUrl, url: newAvatarUrl,
path, ...local,
hash, hash,
}; };
} }

View file

@ -30,6 +30,7 @@ import { isGroupV1 } from '../util/whatTypeOfConversation';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper'; import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
import { sleep } from '../util/sleep'; import { sleep } from '../util/sleep';
import { dropNull } from '../util/dropNull'; import { dropNull } from '../util/dropNull';
import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl';
export async function joinViaLink(value: string): Promise<void> { export async function joinViaLink(value: string): Promise<void> {
let inviteLinkPassword: string; let inviteLinkPassword: string;
@ -184,9 +185,7 @@ export async function joinViaLink(value: string): Promise<void> {
}; };
} else if (localAvatar && localAvatar.path) { } else if (localAvatar && localAvatar.path) {
avatar = { avatar = {
url: window.Signal.Migrations.getAbsoluteAttachmentPath( url: getLocalAttachmentUrl(localAvatar),
localAvatar.path
),
}; };
} }
@ -405,7 +404,7 @@ export async function joinViaLink(value: string): Promise<void> {
if (attributes.avatar && attributes.avatar.path) { if (attributes.avatar && attributes.avatar.path) {
localAvatar = { localAvatar = {
path: attributes.avatar.path, ...attributes.avatar,
}; };
// Dialog has been dismissed; we'll delete the unneeeded avatar // Dialog has been dismissed; we'll delete the unneeeded avatar

View file

@ -23,7 +23,7 @@ export type MinimalConversation = Satisfies<
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'announcementsOnly' | 'announcementsOnly'
| 'areWeAdmin' | 'areWeAdmin'
| 'avatarPath' | 'avatarUrl'
| 'canChangeTimer' | 'canChangeTimer'
| 'color' | 'color'
| 'expireTimer' | 'expireTimer'
@ -43,7 +43,7 @@ export type MinimalConversation = Satisfies<
| 'profileName' | 'profileName'
| 'title' | 'title'
| 'type' | 'type'
| 'unblurredAvatarPath' | 'unblurredAvatarUrl'
> >
>; >;
@ -54,7 +54,7 @@ export function useMinimalConversation(
acceptedMessageRequest, acceptedMessageRequest,
announcementsOnly, announcementsOnly,
areWeAdmin, areWeAdmin,
avatarPath, avatarUrl,
canChangeTimer, canChangeTimer,
color, color,
expireTimer, expireTimer,
@ -74,14 +74,14 @@ export function useMinimalConversation(
profileName, profileName,
title, title,
type, type,
unblurredAvatarPath, unblurredAvatarUrl,
} = conversation; } = conversation;
return useMemo(() => { return useMemo(() => {
return { return {
acceptedMessageRequest, acceptedMessageRequest,
announcementsOnly, announcementsOnly,
areWeAdmin, areWeAdmin,
avatarPath, avatarUrl,
canChangeTimer, canChangeTimer,
color, color,
expireTimer, expireTimer,
@ -101,13 +101,13 @@ export function useMinimalConversation(
profileName, profileName,
title, title,
type, type,
unblurredAvatarPath, unblurredAvatarUrl,
}; };
}, [ }, [
acceptedMessageRequest, acceptedMessageRequest,
announcementsOnly, announcementsOnly,
areWeAdmin, areWeAdmin,
avatarPath, avatarUrl,
canChangeTimer, canChangeTimer,
color, color,
expireTimer, expireTimer,
@ -127,6 +127,6 @@ export function useMinimalConversation(
profileName, profileName,
title, title,
type, type,
unblurredAvatarPath, unblurredAvatarUrl,
]); ]);
} }

View file

@ -2,7 +2,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { existsSync } from 'fs'; import { existsSync } from 'node:fs';
import { PassThrough } from 'node:stream';
import * as durations from '../util/durations'; import * as durations from '../util/durations';
import * as log from '../logging/log'; import * as log from '../logging/log';
@ -22,6 +23,7 @@ import {
type EncryptedAttachmentV2, type EncryptedAttachmentV2,
getAttachmentCiphertextLength, getAttachmentCiphertextLength,
getAesCbcCiphertextLength, getAesCbcCiphertextLength,
decryptAttachmentV2ToSink,
ReencyptedDigestMismatchError, ReencyptedDigestMismatchError,
} from '../AttachmentCrypto'; } from '../AttachmentCrypto';
import { getBackupKey } from '../services/backups/crypto'; import { getBackupKey } from '../services/backups/crypto';
@ -114,6 +116,7 @@ type RunAttachmentBackupJobDependenciesType = {
backupMediaBatch?: WebAPIType['backupMediaBatch']; backupMediaBatch?: WebAPIType['backupMediaBatch'];
backupsService: BackupsService; backupsService: BackupsService;
encryptAndUploadAttachment: typeof encryptAndUploadAttachment; encryptAndUploadAttachment: typeof encryptAndUploadAttachment;
decryptAttachmentV2ToSink: typeof decryptAttachmentV2ToSink;
}; };
export async function runAttachmentBackupJob( export async function runAttachmentBackupJob(
@ -125,6 +128,7 @@ export async function runAttachmentBackupJob(
backupsService, backupsService,
backupMediaBatch: window.textsecure.server?.backupMediaBatch, backupMediaBatch: window.textsecure.server?.backupMediaBatch,
encryptAndUploadAttachment, encryptAndUploadAttachment,
decryptAttachmentV2ToSink,
} }
): Promise<JobManagerJobResultType> { ): Promise<JobManagerJobResultType> {
const jobIdForLogging = getJobIdForLogging(job); const jobIdForLogging = getJobIdForLogging(job);
@ -171,7 +175,8 @@ async function runAttachmentBackupJobInner(
'Only standard uploads are currently supported' 'Only standard uploads are currently supported'
); );
const { path, transitCdnInfo, iv, digest, keys, size } = job.data; const { path, transitCdnInfo, iv, digest, keys, size, version, localKey } =
job.data;
const mediaId = getMediaIdFromMediaName(mediaName); const mediaId = getMediaIdFromMediaName(mediaName);
const backupKeyMaterial = deriveBackupMediaKeyMaterial( const backupKeyMaterial = deriveBackupMediaKeyMaterial(
@ -236,6 +241,10 @@ async function runAttachmentBackupJobInner(
log.info(`${logId}: uploading to transit tier`); log.info(`${logId}: uploading to transit tier`);
const uploadResult = await uploadToTransitTier({ const uploadResult = await uploadToTransitTier({
absolutePath, absolutePath,
version,
localKey,
size,
keys, keys,
iv, iv,
digest, digest,
@ -263,6 +272,9 @@ type UploadResponseType = {
async function uploadToTransitTier({ async function uploadToTransitTier({
absolutePath, absolutePath,
keys, keys,
version,
localKey,
size,
iv, iv,
digest, digest,
logPrefix, logPrefix,
@ -272,13 +284,54 @@ async function uploadToTransitTier({
iv: string; iv: string;
digest: string; digest: string;
keys: string; keys: string;
version?: 2;
localKey?: string;
size: number;
logPrefix: string; logPrefix: string;
dependencies: { dependencies: {
decryptAttachmentV2ToSink: typeof decryptAttachmentV2ToSink;
encryptAndUploadAttachment: typeof encryptAndUploadAttachment; encryptAndUploadAttachment: typeof encryptAndUploadAttachment;
}; };
}): Promise<UploadResponseType> { }): Promise<UploadResponseType> {
try { try {
const uploadResult = await dependencies.encryptAndUploadAttachment({ if (version === 2) {
strictAssert(
localKey != null,
'Missing localKey for version 2 attachment'
);
const sink = new PassThrough();
// This `Promise.all` is chaining two separate pipelines via
// a pass-through `sink`.
const [, result] = await Promise.all([
dependencies.decryptAttachmentV2ToSink(
{
idForLogging: 'uploadToTransitTier',
ciphertextPath: absolutePath,
keysBase64: localKey,
size,
isLocal: true,
},
sink
),
dependencies.encryptAndUploadAttachment({
plaintext: { stream: sink },
keys: fromBase64(keys),
dangerousIv: {
reason: 'reencrypting-for-backup',
iv: fromBase64(iv),
digestToMatch: fromBase64(digest),
},
uploadType: 'backup',
}),
]);
return result;
}
// Legacy attachments
return dependencies.encryptAndUploadAttachment({
plaintext: { absolutePath }, plaintext: { absolutePath },
keys: fromBase64(keys), keys: fromBase64(keys),
dangerousIv: { dangerousIv: {
@ -288,7 +341,6 @@ async function uploadToTransitTier({
}, },
uploadType: 'backup', uploadType: 'backup',
}); });
return uploadResult;
} catch (error) { } catch (error) {
log.error( log.error(
`${logPrefix}/uploadToTransitTier: Error while encrypting and uploading`, `${logPrefix}/uploadToTransitTier: Error while encrypting and uploading`,

3
ts/model-types.d.ts vendored
View file

@ -471,6 +471,9 @@ export type ConversationAttributesType = {
// This value is useless once the message request has been approved. We don't clean it // This value is useless once the message request has been approved. We don't clean it
// up but could. We don't persist it but could (though we'd probably want to clean it // up but could. We don't persist it but could (though we'd probably want to clean it
// up in that case). // up in that case).
unblurredAvatarUrl?: string;
// Legacy field, mapped to above in getConversation()
unblurredAvatarPath?: string; unblurredAvatarPath?: string;
}; };

View file

@ -26,9 +26,14 @@ import { toDayMillis } from '../util/timestamp';
import { areWeAdmin } from '../util/areWeAdmin'; import { areWeAdmin } from '../util/areWeAdmin';
import { isBlocked } from '../util/isBlocked'; import { isBlocked } from '../util/isBlocked';
import { getAboutText } from '../util/getAboutText'; import { getAboutText } from '../util/getAboutText';
import { getAvatarPath } from '../util/avatarUtils'; import {
getAvatar,
getRawAvatarPath,
getLocalAvatarUrl,
} from '../util/avatarUtils';
import { getDraftPreview } from '../util/getDraftPreview'; import { getDraftPreview } from '../util/getDraftPreview';
import { hasDraft } from '../util/hasDraft'; import { hasDraft } from '../util/hasDraft';
import { missingCaseError } from '../util/missingCaseError';
import * as Conversation from '../types/Conversation'; import * as Conversation from '../types/Conversation';
import type { StickerType, StickerWithHydratedData } from '../types/Stickers'; import type { StickerType, StickerWithHydratedData } from '../types/Stickers';
import * as Stickers from '../types/Stickers'; import * as Stickers from '../types/Stickers';
@ -77,6 +82,7 @@ import {
decryptProfileName, decryptProfileName,
deriveAccessKey, deriveAccessKey,
} from '../Crypto'; } from '../Crypto';
import { decryptAttachmentV2 } from '../AttachmentCrypto';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import type { DraftBodyRanges } from '../types/BodyRange'; import type { DraftBodyRanges } from '../types/BodyRange';
import { BodyRange } from '../types/BodyRange'; import { BodyRange } from '../types/BodyRange';
@ -84,6 +90,7 @@ import { migrateColor } from '../util/migrateColor';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import { import {
NotificationType, NotificationType,
NotificationSetting,
notificationService, notificationService,
} from '../services/notifications'; } from '../services/notifications';
import { storageServiceUploadJob } from '../services/storage'; import { storageServiceUploadJob } from '../services/storage';
@ -178,6 +185,7 @@ window.Whisper = window.Whisper || {};
const { Message } = window.Signal.Types; const { Message } = window.Signal.Types;
const { const {
copyIntoTempDirectory,
deleteAttachmentData, deleteAttachmentData,
doesAttachmentExist, doesAttachmentExist,
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
@ -3660,8 +3668,8 @@ export class ConversationModel extends window.Backbone
} }
const { key } = packData; const { key } = packData;
const { emoji, path, width, height } = stickerData; const { emoji, width, height } = stickerData;
const data = await readStickerData(path); const data = await readStickerData(stickerData);
// We need this content type to be an image so we can display an `<img>` instead of a // We need this content type to be an image so we can display an `<img>` instead of a
// `<video>` or an error, but it's not critical that we get the full type correct. // `<video>` or an error, but it's not critical that we get the full type correct.
@ -4736,18 +4744,18 @@ export class ConversationModel extends window.Backbone
} }
async setProfileAvatar( async setProfileAvatar(
avatarPath: undefined | null | string, avatarUrl: undefined | null | string,
decryptionKey: Uint8Array decryptionKey: Uint8Array
): Promise<void> { ): Promise<void> {
if (isMe(this.attributes)) { if (isMe(this.attributes)) {
if (avatarPath) { if (avatarUrl) {
await window.storage.put('avatarUrl', avatarPath); await window.storage.put('avatarUrl', avatarUrl);
} else { } else {
await window.storage.remove('avatarUrl'); await window.storage.remove('avatarUrl');
} }
} }
if (!avatarPath) { if (!avatarUrl) {
this.set({ profileAvatar: undefined }); this.set({ profileAvatar: undefined });
return; return;
} }
@ -4756,7 +4764,7 @@ export class ConversationModel extends window.Backbone
if (!messaging) { if (!messaging) {
throw new Error('setProfileAvatar: Cannot fetch avatar when offline!'); throw new Error('setProfileAvatar: Cannot fetch avatar when offline!');
} }
const avatar = await messaging.getAvatar(avatarPath); const avatar = await messaging.getAvatar(avatarUrl);
// decrypt // decrypt
const decrypted = decryptProfile(avatar, decryptionKey); const decrypted = decryptProfile(avatar, decryptionKey);
@ -5130,11 +5138,11 @@ export class ConversationModel extends window.Backbone
} }
unblurAvatar(): void { unblurAvatar(): void {
const avatarPath = getAvatarPath(this.attributes); const avatarUrl = getRawAvatarPath(this.attributes);
if (avatarPath) { if (avatarUrl) {
this.set('unblurredAvatarPath', avatarPath); this.set('unblurredAvatarUrl', avatarUrl);
} else { } else {
this.unset('unblurredAvatarPath'); this.unset('unblurredAvatarUrl');
} }
} }
@ -5306,16 +5314,36 @@ export class ConversationModel extends window.Backbone
url: string; url: string;
absolutePath?: string; absolutePath?: string;
}> { }> {
const avatarPath = getAvatarPath(this.attributes); let saveToDisk: boolean;
if (avatarPath) {
const notificationSetting = notificationService.getNotificationSetting();
switch (notificationSetting) {
case NotificationSetting.NameOnly:
case NotificationSetting.NameAndMessage:
// According to the MSDN, avatars can only be loaded from disk or an
// http server:
// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-image?redirectedfrom=MSDN
saveToDisk = OS.isWindows();
break;
case NotificationSetting.Off:
case NotificationSetting.NoNameOrMessage:
saveToDisk = false;
break;
default:
throw missingCaseError(notificationSetting);
}
const avatarUrl = getLocalAvatarUrl(this.attributes);
if (avatarUrl) {
return { return {
url: getAbsoluteAttachmentPath(avatarPath), url: avatarUrl,
absolutePath: getAbsoluteAttachmentPath(avatarPath), absolutePath: saveToDisk
? await this.getTemporaryAvatarPath()
: undefined,
}; };
} }
const { url, path } = await this.getIdenticon({ const { url, path } = await this.getIdenticon({
saveToDisk: OS.isWindows(), saveToDisk,
}); });
return { return {
url, url,
@ -5323,6 +5351,46 @@ export class ConversationModel extends window.Backbone
}; };
} }
private async getTemporaryAvatarPath(): Promise<string | undefined> {
const avatar = getAvatar(this.attributes);
if (avatar?.path == null) {
return undefined;
}
const avatarPath = getRawAvatarPath(this.attributes);
if (!avatarPath) {
return undefined;
}
// Already plaintext
if (avatar.version !== 2) {
return avatarPath;
}
if (!avatar.localKey || !avatar.size) {
return undefined;
}
const { path: plaintextPath } = await decryptAttachmentV2({
ciphertextPath: avatarPath,
idForLogging: 'getAvatarOrIdenticon',
keysBase64: avatar.localKey,
size: avatar.size,
getAbsoluteAttachmentPath,
isLocal: true,
});
try {
const { path: tempPath } = await copyIntoTempDirectory(
getAbsoluteAttachmentPath(plaintextPath)
);
return getAbsoluteTempPath(tempPath);
} finally {
await deleteAttachmentData(plaintextPath);
}
}
private async getIdenticon({ private async getIdenticon({
saveToDisk, saveToDisk,
}: { saveToDisk?: boolean } = {}): Promise<{ }: { saveToDisk?: boolean } = {}): Promise<{

View file

@ -284,7 +284,7 @@ export class MentionCompletion {
> >
<Avatar <Avatar
acceptedMessageRequest={member.acceptedMessageRequest} acceptedMessageRequest={member.acceptedMessageRequest}
avatarPath={member.avatarPath} avatarUrl={member.avatarUrl}
badge={getPreferredBadge(member.badges)} badge={getPreferredBadge(member.badges)}
conversationType="direct" conversationType="direct"
i18n={this.options.i18n} i18n={this.options.i18n}
@ -293,7 +293,7 @@ export class MentionCompletion {
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}
theme={theme} theme={theme}
title={member.title} title={member.title}
unblurredAvatarPath={member.unblurredAvatarPath} unblurredAvatarUrl={member.unblurredAvatarUrl}
/> />
<div className="module-composition-input__suggestions__title"> <div className="module-composition-input__suggestions__title">
<UserText text={member.title} /> <UserText text={member.title} />

View file

@ -449,8 +449,8 @@ async function getStickerPackPreview(
const sticker = pack.stickers[coverStickerId]; const sticker = pack.stickers[coverStickerId];
const data = const data =
pack.status === 'ephemeral' pack.status === 'ephemeral'
? await window.Signal.Migrations.readTempData(sticker.path) ? await window.Signal.Migrations.readTempData(sticker)
: await window.Signal.Migrations.readStickerData(sticker.path); : await window.Signal.Migrations.readStickerData(sticker);
if (abortSignal.aborted) { if (abortSignal.aborted) {
return null; return null;

View file

@ -20,15 +20,13 @@ import { DelimitedStream } from '../../util/DelimitedStream';
import { appendPaddingStream } from '../../util/logPadding'; import { appendPaddingStream } from '../../util/logPadding';
import { prependStream } from '../../util/prependStream'; import { prependStream } from '../../util/prependStream';
import { appendMacStream } from '../../util/appendMacStream'; import { appendMacStream } from '../../util/appendMacStream';
import { getIvAndDecipher } from '../../util/getIvAndDecipher';
import { getMacAndUpdateHmac } from '../../util/getMacAndUpdateHmac';
import { HOUR } from '../../util/durations'; import { HOUR } from '../../util/durations';
import { CipherType, HashType } from '../../types/Crypto'; import { CipherType, HashType } from '../../types/Crypto';
import * as Errors from '../../types/errors'; import * as Errors from '../../types/errors';
import { constantTimeEqual } from '../../Crypto'; import { constantTimeEqual } from '../../Crypto';
import { import { measureSize } from '../../AttachmentCrypto';
getIvAndDecipher,
getMacAndUpdateHmac,
measureSize,
} from '../../AttachmentCrypto';
import { BackupExportStream } from './export'; import { BackupExportStream } from './export';
import { BackupImportStream } from './import'; import { BackupImportStream } from './import';
import { getKeyMaterial } from './crypto'; import { getKeyMaterial } from './crypto';

View file

@ -171,6 +171,10 @@ export function convertBackupMessageAttachmentToAttachment(
async function generateNewEncryptionInfoForAttachment( async function generateNewEncryptionInfoForAttachment(
attachment: Readonly<LocallySavedAttachment> attachment: Readonly<LocallySavedAttachment>
): Promise<AttachmentReadyForBackup> { ): Promise<AttachmentReadyForBackup> {
strictAssert(
attachment.version !== 2,
'generateNewEncryptionInfoForAttachment can only be used on legacy attachments'
);
const fixedUpAttachment = { ...attachment }; const fixedUpAttachment = { ...attachment };
// Since we are changing the encryption, we need to delete all encryption & location // Since we are changing the encryption, we need to delete all encryption & location
@ -195,6 +199,8 @@ async function generateNewEncryptionInfoForAttachment(
attachment.path attachment.path
), ),
}, },
getAbsoluteAttachmentPath:
window.Signal.Migrations.getAbsoluteAttachmentPath,
}); });
return { return {

Some files were not shown because too many files have changed in this diff Show more