New attachment storage system
This commit is contained in:
parent
273e1ccb15
commit
28664a606f
161 changed files with 2418 additions and 1562 deletions
|
@ -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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
12
app/main.ts
12
app/main.ts
|
@ -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.
|
||||||
|
|
|
@ -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';"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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" />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}')`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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' },
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 =>
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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({});
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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',
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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.',
|
||||||
|
|
|
@ -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}`,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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]}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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 })}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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 })}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
36
ts/groups.ts
36
ts/groups.ts
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
3
ts/model-types.d.ts
vendored
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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<{
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue