Optimize profile avatar uploads and sync urls
This commit is contained in:
parent
703bb8a3a3
commit
36ce4f27a2
15 changed files with 147 additions and 77 deletions
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
|
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
@ -46,10 +46,6 @@ export const AvatarPreview = ({
|
||||||
onClick,
|
onClick,
|
||||||
style = {},
|
style = {},
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const startingAvatarPathRef = useRef<undefined | string>(
|
|
||||||
avatarValue ? undefined : avatarPath
|
|
||||||
);
|
|
||||||
|
|
||||||
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 avatarPath if one is provided, but only if we're in editable mode.
|
||||||
|
@ -60,8 +56,7 @@ export const AvatarPreview = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startingAvatarPath = startingAvatarPathRef.current;
|
if (!avatarPath) {
|
||||||
if (!startingAvatarPath) {
|
|
||||||
return noop;
|
return noop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,14 +64,12 @@ export const AvatarPreview = ({
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const buffer = await imagePathToBytes(startingAvatarPath);
|
const buffer = await imagePathToBytes(avatarPath);
|
||||||
if (shouldCancel) {
|
if (shouldCancel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAvatarPreview(buffer);
|
setAvatarPreview(buffer);
|
||||||
if (onAvatarLoaded) {
|
onAvatarLoaded?.(buffer);
|
||||||
onAvatarLoaded(buffer);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (shouldCancel) {
|
if (shouldCancel) {
|
||||||
return;
|
return;
|
||||||
|
@ -92,7 +85,7 @@ export const AvatarPreview = ({
|
||||||
return () => {
|
return () => {
|
||||||
shouldCancel = true;
|
shouldCancel = true;
|
||||||
};
|
};
|
||||||
}, [onAvatarLoaded, isEditable]);
|
}, [avatarPath, onAvatarLoaded, isEditable]);
|
||||||
|
|
||||||
// Ensures that when avatarValue changes we generate new URLs
|
// Ensures that when avatarValue changes we generate new URLs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -25,7 +25,7 @@ const stories = storiesOf('Components/ProfileEditor', module);
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
aboutEmoji: overrideProps.aboutEmoji,
|
aboutEmoji: overrideProps.aboutEmoji,
|
||||||
aboutText: text('about', overrideProps.aboutText || ''),
|
aboutText: text('about', overrideProps.aboutText || ''),
|
||||||
avatarPath: overrideProps.avatarPath,
|
profileAvatarPath: overrideProps.profileAvatarPath,
|
||||||
clearUsernameSave: action('clearUsernameSave'),
|
clearUsernameSave: action('clearUsernameSave'),
|
||||||
conversationId: '123',
|
conversationId: '123',
|
||||||
color: overrideProps.color || getRandomColor(),
|
color: overrideProps.color || getRandomColor(),
|
||||||
|
@ -64,7 +64,7 @@ stories.add('Full Set', () => {
|
||||||
{...createProps({
|
{...createProps({
|
||||||
aboutEmoji: '🙏',
|
aboutEmoji: '🙏',
|
||||||
aboutText: 'Live. Laugh. Love',
|
aboutText: 'Live. Laugh. Love',
|
||||||
avatarPath: '/fixtures/kitten-3-64-64.jpg',
|
profileAvatarPath: '/fixtures/kitten-3-64-64.jpg',
|
||||||
onSetSkinTone: setSkinTone,
|
onSetSkinTone: setSkinTone,
|
||||||
familyName: getLastName(),
|
familyName: getLastName(),
|
||||||
skinTone,
|
skinTone,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type { AvatarColorType } from '../types/Colors';
|
||||||
import { AvatarColors } from '../types/Colors';
|
import { AvatarColors } from '../types/Colors';
|
||||||
import type {
|
import type {
|
||||||
AvatarDataType,
|
AvatarDataType,
|
||||||
|
AvatarUpdateType,
|
||||||
DeleteAvatarFromDiskActionType,
|
DeleteAvatarFromDiskActionType,
|
||||||
ReplaceAvatarActionType,
|
ReplaceAvatarActionType,
|
||||||
SaveAvatarToDiskActionType,
|
SaveAvatarToDiskActionType,
|
||||||
|
@ -58,14 +59,14 @@ type PropsExternalType = {
|
||||||
onEditStateChanged: (editState: EditState) => unknown;
|
onEditStateChanged: (editState: EditState) => unknown;
|
||||||
onProfileChanged: (
|
onProfileChanged: (
|
||||||
profileData: ProfileDataType,
|
profileData: ProfileDataType,
|
||||||
avatarBuffer?: Uint8Array
|
avatar: AvatarUpdateType
|
||||||
) => unknown;
|
) => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
aboutEmoji?: string;
|
aboutEmoji?: string;
|
||||||
aboutText?: string;
|
aboutText?: string;
|
||||||
avatarPath?: string;
|
profileAvatarPath?: string;
|
||||||
color?: AvatarColorType;
|
color?: AvatarColorType;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
familyName?: string;
|
familyName?: string;
|
||||||
|
@ -211,7 +212,7 @@ function mapSaveStateToEditState({
|
||||||
export const ProfileEditor = ({
|
export const ProfileEditor = ({
|
||||||
aboutEmoji,
|
aboutEmoji,
|
||||||
aboutText,
|
aboutText,
|
||||||
avatarPath,
|
profileAvatarPath,
|
||||||
clearUsernameSave,
|
clearUsernameSave,
|
||||||
color,
|
color,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -254,9 +255,16 @@ export const ProfileEditor = ({
|
||||||
UsernameEditState.Editing
|
UsernameEditState.Editing
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [startingAvatarPath, setStartingAvatarPath] =
|
||||||
|
useState(profileAvatarPath);
|
||||||
|
|
||||||
|
const [oldAvatarBuffer, setOldAvatarBuffer] = useState<
|
||||||
|
Uint8Array | undefined
|
||||||
|
>(undefined);
|
||||||
const [avatarBuffer, setAvatarBuffer] = useState<Uint8Array | undefined>(
|
const [avatarBuffer, setAvatarBuffer] = useState<Uint8Array | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
const [isLoadingAvatar, setIsLoadingAvatar] = useState(true);
|
||||||
const [stagedProfile, setStagedProfile] = useState<ProfileDataType>({
|
const [stagedProfile, setStagedProfile] = useState<ProfileDataType>({
|
||||||
aboutEmoji,
|
aboutEmoji,
|
||||||
aboutText,
|
aboutText,
|
||||||
|
@ -285,6 +293,9 @@ export const ProfileEditor = ({
|
||||||
// To make AvatarEditor re-render less often
|
// To make AvatarEditor re-render less often
|
||||||
const handleAvatarChanged = useCallback(
|
const handleAvatarChanged = useCallback(
|
||||||
(avatar: Uint8Array | undefined) => {
|
(avatar: Uint8Array | undefined) => {
|
||||||
|
// Do not display stale avatar from disk anymore.
|
||||||
|
setStartingAvatarPath(undefined);
|
||||||
|
|
||||||
setAvatarBuffer(avatar);
|
setAvatarBuffer(avatar);
|
||||||
setEditState(EditState.None);
|
setEditState(EditState.None);
|
||||||
onProfileChanged(
|
onProfileChanged(
|
||||||
|
@ -295,10 +306,11 @@ export const ProfileEditor = ({
|
||||||
? trim(stagedProfile.familyName)
|
? trim(stagedProfile.familyName)
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
avatar
|
{ oldAvatar: oldAvatarBuffer, newAvatar: avatar }
|
||||||
);
|
);
|
||||||
|
setOldAvatarBuffer(avatar);
|
||||||
},
|
},
|
||||||
[onProfileChanged, stagedProfile]
|
[onProfileChanged, stagedProfile, oldAvatarBuffer]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getFullNameText = () => {
|
const getFullNameText = () => {
|
||||||
|
@ -405,9 +417,14 @@ export const ProfileEditor = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
// To make AvatarEditor re-render less often
|
// To make AvatarEditor re-render less often
|
||||||
const handleAvatarLoaded = useCallback(avatar => {
|
const handleAvatarLoaded = useCallback(
|
||||||
setAvatarBuffer(avatar);
|
avatar => {
|
||||||
}, []);
|
setAvatarBuffer(avatar);
|
||||||
|
setOldAvatarBuffer(avatar);
|
||||||
|
setIsLoadingAvatar(false);
|
||||||
|
},
|
||||||
|
[setAvatarBuffer, setOldAvatarBuffer, setIsLoadingAvatar]
|
||||||
|
);
|
||||||
|
|
||||||
let content: JSX.Element;
|
let content: JSX.Element;
|
||||||
|
|
||||||
|
@ -415,7 +432,7 @@ export const ProfileEditor = ({
|
||||||
content = (
|
content = (
|
||||||
<AvatarEditor
|
<AvatarEditor
|
||||||
avatarColor={color || AvatarColors[0]}
|
avatarColor={color || AvatarColors[0]}
|
||||||
avatarPath={avatarPath}
|
avatarPath={startingAvatarPath}
|
||||||
avatarValue={avatarBuffer}
|
avatarValue={avatarBuffer}
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
conversationTitle={getFullNameText()}
|
conversationTitle={getFullNameText()}
|
||||||
|
@ -430,6 +447,7 @@ export const ProfileEditor = ({
|
||||||
);
|
);
|
||||||
} else if (editState === EditState.ProfileName) {
|
} else if (editState === EditState.ProfileName) {
|
||||||
const shouldDisableSave =
|
const shouldDisableSave =
|
||||||
|
isLoadingAvatar ||
|
||||||
!stagedProfile.firstName ||
|
!stagedProfile.firstName ||
|
||||||
(stagedProfile.firstName === fullName.firstName &&
|
(stagedProfile.firstName === fullName.firstName &&
|
||||||
stagedProfile.familyName === fullName.familyName) ||
|
stagedProfile.familyName === fullName.familyName) ||
|
||||||
|
@ -502,7 +520,10 @@ export const ProfileEditor = ({
|
||||||
familyName: stagedProfile.familyName,
|
familyName: stagedProfile.familyName,
|
||||||
});
|
});
|
||||||
|
|
||||||
onProfileChanged(stagedProfile, avatarBuffer);
|
onProfileChanged(stagedProfile, {
|
||||||
|
oldAvatar: oldAvatarBuffer,
|
||||||
|
newAvatar: avatarBuffer,
|
||||||
|
});
|
||||||
handleBack();
|
handleBack();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -513,8 +534,9 @@ export const ProfileEditor = ({
|
||||||
);
|
);
|
||||||
} else if (editState === EditState.Bio) {
|
} else if (editState === EditState.Bio) {
|
||||||
const shouldDisableSave =
|
const shouldDisableSave =
|
||||||
stagedProfile.aboutText === fullBio.aboutText &&
|
isLoadingAvatar ||
|
||||||
stagedProfile.aboutEmoji === fullBio.aboutEmoji;
|
(stagedProfile.aboutText === fullBio.aboutText &&
|
||||||
|
stagedProfile.aboutEmoji === fullBio.aboutEmoji);
|
||||||
|
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
|
@ -613,7 +635,10 @@ export const ProfileEditor = ({
|
||||||
aboutText: stagedProfile.aboutText,
|
aboutText: stagedProfile.aboutText,
|
||||||
});
|
});
|
||||||
|
|
||||||
onProfileChanged(stagedProfile, avatarBuffer);
|
onProfileChanged(stagedProfile, {
|
||||||
|
oldAvatar: oldAvatarBuffer,
|
||||||
|
newAvatar: avatarBuffer,
|
||||||
|
});
|
||||||
handleBack();
|
handleBack();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -689,7 +714,7 @@ export const ProfileEditor = ({
|
||||||
<>
|
<>
|
||||||
<AvatarPreview
|
<AvatarPreview
|
||||||
avatarColor={color}
|
avatarColor={color}
|
||||||
avatarPath={avatarPath}
|
avatarPath={startingAvatarPath}
|
||||||
avatarValue={avatarBuffer}
|
avatarValue={avatarBuffer}
|
||||||
conversationTitle={getFullNameText()}
|
conversationTitle={getFullNameText()}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import type { PropsType as ProfileEditorPropsType } from './ProfileEditor';
|
import type { PropsType as ProfileEditorPropsType } from './ProfileEditor';
|
||||||
import { ProfileEditor, EditState } from './ProfileEditor';
|
import { ProfileEditor, EditState } from './ProfileEditor';
|
||||||
import type { ProfileDataType } from '../state/ducks/conversations';
|
import type { ProfileDataType } from '../state/ducks/conversations';
|
||||||
|
import type { AvatarUpdateType } from '../types/Avatar';
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
|
@ -15,7 +16,7 @@ export type PropsDataType = {
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
myProfileChanged: (
|
myProfileChanged: (
|
||||||
profileData: ProfileDataType,
|
profileData: ProfileDataType,
|
||||||
avatarBuffer?: Uint8Array
|
avatar: AvatarUpdateType
|
||||||
) => unknown;
|
) => unknown;
|
||||||
toggleProfileEditor: () => unknown;
|
toggleProfileEditor: () => unknown;
|
||||||
toggleProfileEditorHasError: () => unknown;
|
toggleProfileEditorHasError: () => unknown;
|
||||||
|
|
|
@ -1846,6 +1846,7 @@ export class ConversationModel extends window.Backbone
|
||||||
avatarPath: this.getAbsoluteAvatarPath(),
|
avatarPath: this.getAbsoluteAvatarPath(),
|
||||||
avatarHash: this.getAvatarHash(),
|
avatarHash: this.getAvatarHash(),
|
||||||
unblurredAvatarPath: this.getAbsoluteUnblurredAvatarPath(),
|
unblurredAvatarPath: this.getAbsoluteUnblurredAvatarPath(),
|
||||||
|
profileAvatarPath: this.getAbsoluteProfileAvatarPath(),
|
||||||
color,
|
color,
|
||||||
conversationColor: this.getConversationColor(),
|
conversationColor: this.getConversationColor(),
|
||||||
customColor,
|
customColor,
|
||||||
|
@ -5019,6 +5020,11 @@ export class ConversationModel extends window.Backbone
|
||||||
return avatarPath ? getAbsoluteAttachmentPath(avatarPath) : undefined;
|
return avatarPath ? getAbsoluteAttachmentPath(avatarPath) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAbsoluteProfileAvatarPath(): string | undefined {
|
||||||
|
const avatarPath = this.get('profileAvatar')?.path;
|
||||||
|
return avatarPath ? getAbsoluteAttachmentPath(avatarPath) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
getAbsoluteUnblurredAvatarPath(): string | undefined {
|
getAbsoluteUnblurredAvatarPath(): string | undefined {
|
||||||
const unblurredAvatarPath = this.get('unblurredAvatarPath');
|
const unblurredAvatarPath = this.get('unblurredAvatarPath');
|
||||||
return unblurredAvatarPath
|
return unblurredAvatarPath
|
||||||
|
|
|
@ -178,7 +178,10 @@ export function toAccountRecord(
|
||||||
if (conversation.get('profileFamilyName')) {
|
if (conversation.get('profileFamilyName')) {
|
||||||
accountRecord.familyName = conversation.get('profileFamilyName') || '';
|
accountRecord.familyName = conversation.get('profileFamilyName') || '';
|
||||||
}
|
}
|
||||||
accountRecord.avatarUrl = window.storage.get('avatarUrl') || '';
|
const avatarUrl = window.storage.get('avatarUrl');
|
||||||
|
if (avatarUrl !== undefined) {
|
||||||
|
accountRecord.avatarUrl = avatarUrl;
|
||||||
|
}
|
||||||
accountRecord.noteToSelfArchived = Boolean(conversation.get('isArchived'));
|
accountRecord.noteToSelfArchived = Boolean(conversation.get('isArchived'));
|
||||||
accountRecord.noteToSelfMarkedUnread = Boolean(
|
accountRecord.noteToSelfMarkedUnread = Boolean(
|
||||||
conversation.get('markedUnread')
|
conversation.get('markedUnread')
|
||||||
|
@ -895,7 +898,6 @@ export async function mergeAccountRecord(
|
||||||
): Promise<MergeResultType> {
|
): Promise<MergeResultType> {
|
||||||
let details = new Array<string>();
|
let details = new Array<string>();
|
||||||
const {
|
const {
|
||||||
avatarUrl,
|
|
||||||
linkPreviews,
|
linkPreviews,
|
||||||
notDiscoverableByPhoneNumber,
|
notDiscoverableByPhoneNumber,
|
||||||
noteToSelfArchived,
|
noteToSelfArchived,
|
||||||
|
@ -1146,10 +1148,9 @@ export async function mergeAccountRecord(
|
||||||
{ viaStorageServiceSync: true }
|
{ viaStorageServiceSync: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (avatarUrl) {
|
const avatarUrl = dropNull(accountRecord.avatarUrl);
|
||||||
await conversation.setProfileAvatar(avatarUrl, profileKey);
|
await conversation.setProfileAvatar(avatarUrl, profileKey);
|
||||||
window.storage.put('avatarUrl', avatarUrl);
|
window.storage.put('avatarUrl', avatarUrl);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
|
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
|
||||||
|
|
|
@ -11,10 +11,11 @@ import { getProfile } from '../util/getProfile';
|
||||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import { isWhitespace } from '../util/whitespaceStringUtil';
|
import { isWhitespace } from '../util/whitespaceStringUtil';
|
||||||
|
import type { AvatarUpdateType } from '../types/Avatar';
|
||||||
|
|
||||||
export async function writeProfile(
|
export async function writeProfile(
|
||||||
conversation: ConversationType,
|
conversation: ConversationType,
|
||||||
avatarBuffer?: Uint8Array
|
avatar: AvatarUpdateType
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Before we write anything we request the user's profile so that we can
|
// Before we write anything we request the user's profile so that we can
|
||||||
// have an up-to-date paymentAddress to be able to include it when we write
|
// have an up-to-date paymentAddress to be able to include it when we write
|
||||||
|
@ -41,7 +42,7 @@ export async function writeProfile(
|
||||||
|
|
||||||
const [profileData, encryptedAvatarData] = await encryptProfileData(
|
const [profileData, encryptedAvatarData] = await encryptProfileData(
|
||||||
conversation,
|
conversation,
|
||||||
avatarBuffer
|
avatar
|
||||||
);
|
);
|
||||||
const avatarRequestHeaders = await window.textsecure.messaging.putProfile(
|
const avatarRequestHeaders = await window.textsecure.messaging.putProfile(
|
||||||
profileData
|
profileData
|
||||||
|
@ -50,37 +51,49 @@ export async function writeProfile(
|
||||||
// Upload the avatar if provided
|
// Upload the avatar if provided
|
||||||
// delete existing files on disk if avatar has been removed
|
// delete existing files on disk if avatar has been removed
|
||||||
// update the account's avatar path and hash if it's a new avatar
|
// update the account's avatar path and hash if it's a new avatar
|
||||||
let profileAvatar:
|
const { newAvatar } = avatar;
|
||||||
| {
|
let maybeProfileAvatarUpdate: {
|
||||||
hash: string;
|
profileAvatar?:
|
||||||
path: string;
|
| {
|
||||||
}
|
hash: string;
|
||||||
| undefined;
|
path: string;
|
||||||
if (avatarRequestHeaders && encryptedAvatarData && avatarBuffer) {
|
}
|
||||||
await window.textsecure.messaging.uploadAvatar(
|
| undefined;
|
||||||
|
} = {};
|
||||||
|
if (profileData.sameAvatar) {
|
||||||
|
log.info('writeProfile: not updating avatar');
|
||||||
|
} else if (avatarRequestHeaders && encryptedAvatarData && newAvatar) {
|
||||||
|
log.info('writeProfile: uploading new avatar');
|
||||||
|
const avatarUrl = await window.textsecure.messaging.uploadAvatar(
|
||||||
avatarRequestHeaders,
|
avatarRequestHeaders,
|
||||||
encryptedAvatarData
|
encryptedAvatarData
|
||||||
);
|
);
|
||||||
|
|
||||||
const hash = await computeHash(avatarBuffer);
|
const hash = await computeHash(newAvatar);
|
||||||
|
|
||||||
if (hash !== avatarHash) {
|
if (hash !== avatarHash) {
|
||||||
|
log.info('writeProfile: removing old avatar and saving the new one');
|
||||||
const [path] = await Promise.all([
|
const [path] = await Promise.all([
|
||||||
window.Signal.Migrations.writeNewAttachmentData(avatarBuffer),
|
window.Signal.Migrations.writeNewAttachmentData(newAvatar),
|
||||||
avatarPath
|
avatarPath
|
||||||
? window.Signal.Migrations.deleteAttachmentData(avatarPath)
|
? window.Signal.Migrations.deleteAttachmentData(avatarPath)
|
||||||
: undefined,
|
: undefined,
|
||||||
]);
|
]);
|
||||||
profileAvatar = {
|
maybeProfileAvatarUpdate = {
|
||||||
hash,
|
profileAvatar: { hash, path },
|
||||||
path,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (avatarPath) {
|
|
||||||
await window.Signal.Migrations.deleteAttachmentData(avatarPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const profileAvatarData = profileAvatar ? { profileAvatar } : {};
|
await window.storage.put('avatarUrl', avatarUrl);
|
||||||
|
} else if (avatarPath) {
|
||||||
|
log.info('writeProfile: removing avatar');
|
||||||
|
await Promise.all([
|
||||||
|
window.Signal.Migrations.deleteAttachmentData(avatarPath),
|
||||||
|
window.storage.put('avatarUrl', undefined),
|
||||||
|
]);
|
||||||
|
|
||||||
|
maybeProfileAvatarUpdate = { profileAvatar: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
// Update backbone, update DB, run storage service upload
|
// Update backbone, update DB, run storage service upload
|
||||||
model.set({
|
model.set({
|
||||||
|
@ -88,7 +101,7 @@ export async function writeProfile(
|
||||||
aboutEmoji,
|
aboutEmoji,
|
||||||
profileName: firstName,
|
profileName: firstName,
|
||||||
profileFamilyName: familyName,
|
profileFamilyName: familyName,
|
||||||
...profileAvatarData,
|
...maybeProfileAvatarUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
dataInterface.updateConversation(model.attributes);
|
dataInterface.updateConversation(model.attributes);
|
||||||
|
|
|
@ -63,7 +63,7 @@ import {
|
||||||
getMe,
|
getMe,
|
||||||
getUsernameSaveState,
|
getUsernameSaveState,
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
import type { AvatarDataType } from '../../types/Avatar';
|
import type { AvatarDataType, AvatarUpdateType } from '../../types/Avatar';
|
||||||
import { getDefaultAvatars } from '../../types/Avatar';
|
import { getDefaultAvatars } from '../../types/Avatar';
|
||||||
import { getAvatarData } from '../../util/getAvatarData';
|
import { getAvatarData } from '../../util/getAvatarData';
|
||||||
import { isSameAvatarData } from '../../util/isSameAvatarData';
|
import { isSameAvatarData } from '../../util/isSameAvatarData';
|
||||||
|
@ -121,6 +121,7 @@ export type ConversationType = {
|
||||||
avatars?: Array<AvatarDataType>;
|
avatars?: Array<AvatarDataType>;
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
avatarHash?: string;
|
avatarHash?: string;
|
||||||
|
profileAvatarPath?: string;
|
||||||
unblurredAvatarPath?: string;
|
unblurredAvatarPath?: string;
|
||||||
areWeAdmin?: boolean;
|
areWeAdmin?: boolean;
|
||||||
areWePending?: boolean;
|
areWePending?: boolean;
|
||||||
|
@ -1095,7 +1096,7 @@ function saveUsername({
|
||||||
|
|
||||||
function myProfileChanged(
|
function myProfileChanged(
|
||||||
profileData: ProfileDataType,
|
profileData: ProfileDataType,
|
||||||
avatarBuffer?: Uint8Array
|
avatar: AvatarUpdateType
|
||||||
): ThunkAction<
|
): ThunkAction<
|
||||||
void,
|
void,
|
||||||
RootStateType,
|
RootStateType,
|
||||||
|
@ -1111,7 +1112,7 @@ function myProfileChanged(
|
||||||
...conversation,
|
...conversation,
|
||||||
...profileData,
|
...profileData,
|
||||||
},
|
},
|
||||||
avatarBuffer
|
avatar
|
||||||
);
|
);
|
||||||
|
|
||||||
// writeProfile above updates the backbone model which in turn updates
|
// writeProfile above updates the backbone model which in turn updates
|
||||||
|
|
|
@ -17,7 +17,7 @@ function mapStateToProps(
|
||||||
): Omit<PropsDataType, 'onEditStateChange' | 'onProfileChanged'> &
|
): Omit<PropsDataType, 'onEditStateChange' | 'onProfileChanged'> &
|
||||||
ProfileEditorModalPropsType {
|
ProfileEditorModalPropsType {
|
||||||
const {
|
const {
|
||||||
avatarPath,
|
profileAvatarPath,
|
||||||
avatars: userAvatarData = [],
|
avatars: userAvatarData = [],
|
||||||
aboutText,
|
aboutText,
|
||||||
aboutEmoji,
|
aboutEmoji,
|
||||||
|
@ -34,7 +34,7 @@ function mapStateToProps(
|
||||||
return {
|
return {
|
||||||
aboutEmoji,
|
aboutEmoji,
|
||||||
aboutText,
|
aboutText,
|
||||||
avatarPath,
|
profileAvatarPath,
|
||||||
color,
|
color,
|
||||||
conversationId,
|
conversationId,
|
||||||
familyName,
|
familyName,
|
||||||
|
|
|
@ -10,13 +10,17 @@ import {
|
||||||
decryptProfileName,
|
decryptProfileName,
|
||||||
decryptProfile,
|
decryptProfile,
|
||||||
} from '../../Crypto';
|
} from '../../Crypto';
|
||||||
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
import { UUID } from '../../types/UUID';
|
import { UUID } from '../../types/UUID';
|
||||||
import { encryptProfileData } from '../../util/encryptProfileData';
|
import { encryptProfileData } from '../../util/encryptProfileData';
|
||||||
|
|
||||||
describe('encryptProfileData', () => {
|
describe('encryptProfileData', () => {
|
||||||
it('encrypts and decrypts properly', async () => {
|
let keyBuffer: Uint8Array;
|
||||||
const keyBuffer = getRandomBytes(32);
|
let conversation: ConversationType;
|
||||||
const conversation = {
|
|
||||||
|
beforeEach(() => {
|
||||||
|
keyBuffer = getRandomBytes(32);
|
||||||
|
conversation = {
|
||||||
aboutEmoji: '🐢',
|
aboutEmoji: '🐢',
|
||||||
aboutText: 'I like turtles',
|
aboutText: 'I like turtles',
|
||||||
familyName: 'Kid',
|
familyName: 'Kid',
|
||||||
|
@ -33,8 +37,13 @@ describe('encryptProfileData', () => {
|
||||||
title: '',
|
title: '',
|
||||||
type: 'direct' as const,
|
type: 'direct' as const,
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const [encrypted] = await encryptProfileData(conversation);
|
it('encrypts and decrypts properly', async () => {
|
||||||
|
const [encrypted] = await encryptProfileData(conversation, {
|
||||||
|
oldAvatar: undefined,
|
||||||
|
newAvatar: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
assert.isDefined(encrypted.version);
|
assert.isDefined(encrypted.version);
|
||||||
assert.isDefined(encrypted.name);
|
assert.isDefined(encrypted.name);
|
||||||
|
@ -83,4 +92,22 @@ describe('encryptProfileData', () => {
|
||||||
assert.isDefined(encrypted.aboutEmoji);
|
assert.isDefined(encrypted.aboutEmoji);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sets sameAvatar to true when avatars are the same', async () => {
|
||||||
|
const [encrypted] = await encryptProfileData(conversation, {
|
||||||
|
oldAvatar: new Uint8Array([1, 2, 3]),
|
||||||
|
newAvatar: new Uint8Array([1, 2, 3]),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.isTrue(encrypted.sameAvatar);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets sameAvatar to false when avatars are different', async () => {
|
||||||
|
const [encrypted] = await encryptProfileData(conversation, {
|
||||||
|
oldAvatar: new Uint8Array([1, 2, 3]),
|
||||||
|
newAvatar: new Uint8Array([4, 5, 6, 7]),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.isFalse(encrypted.sameAvatar);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -701,6 +701,7 @@ export type ProfileRequestDataType = {
|
||||||
about: string | null;
|
about: string | null;
|
||||||
aboutEmoji: string | null;
|
aboutEmoji: string | null;
|
||||||
avatar: boolean;
|
avatar: boolean;
|
||||||
|
sameAvatar: boolean;
|
||||||
commitment: string;
|
commitment: string;
|
||||||
name: string;
|
name: string;
|
||||||
paymentAddress: string | null;
|
paymentAddress: string | null;
|
||||||
|
|
|
@ -65,6 +65,11 @@ export type SaveAvatarToDiskActionType = (
|
||||||
conversationId?: string
|
conversationId?: string
|
||||||
) => unknown;
|
) => unknown;
|
||||||
|
|
||||||
|
export type AvatarUpdateType = Readonly<{
|
||||||
|
oldAvatar: Uint8Array | undefined;
|
||||||
|
newAvatar: Uint8Array | undefined;
|
||||||
|
}>;
|
||||||
|
|
||||||
const groupIconColors = [
|
const groupIconColors = [
|
||||||
'A180',
|
'A180',
|
||||||
'A120',
|
'A120',
|
||||||
|
|
2
ts/types/Storage.d.ts
vendored
2
ts/types/Storage.d.ts
vendored
|
@ -107,7 +107,7 @@ export type StorageAccessType = {
|
||||||
typingIndicators: boolean;
|
typingIndicators: boolean;
|
||||||
sealedSenderIndicators: boolean;
|
sealedSenderIndicators: boolean;
|
||||||
storageFetchComplete: boolean;
|
storageFetchComplete: boolean;
|
||||||
avatarUrl: string;
|
avatarUrl: string | undefined;
|
||||||
manifestVersion: number;
|
manifestVersion: number;
|
||||||
storageCredentials: StorageServiceCredentials;
|
storageCredentials: StorageServiceCredentials;
|
||||||
'storage-service-error-records': Array<UnknownRecord>;
|
'storage-service-error-records': Array<UnknownRecord>;
|
||||||
|
|
|
@ -10,11 +10,12 @@ import {
|
||||||
encryptProfile,
|
encryptProfile,
|
||||||
encryptProfileItemWithPadding,
|
encryptProfileItemWithPadding,
|
||||||
} from '../Crypto';
|
} from '../Crypto';
|
||||||
|
import type { AvatarUpdateType } from '../types/Avatar';
|
||||||
import { deriveProfileKeyCommitment, deriveProfileKeyVersion } from './zkgroup';
|
import { deriveProfileKeyCommitment, deriveProfileKeyVersion } from './zkgroup';
|
||||||
|
|
||||||
export async function encryptProfileData(
|
export async function encryptProfileData(
|
||||||
conversation: ConversationType,
|
conversation: ConversationType,
|
||||||
avatarBuffer?: Uint8Array
|
{ oldAvatar, newAvatar }: AvatarUpdateType
|
||||||
): Promise<[ProfileRequestDataType, Uint8Array | undefined]> {
|
): Promise<[ProfileRequestDataType, Uint8Array | undefined]> {
|
||||||
const {
|
const {
|
||||||
aboutEmoji,
|
aboutEmoji,
|
||||||
|
@ -55,10 +56,12 @@ export async function encryptProfileData(
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const encryptedAvatarData = avatarBuffer
|
const encryptedAvatarData = newAvatar
|
||||||
? encryptProfile(avatarBuffer, keyBuffer)
|
? encryptProfile(newAvatar, keyBuffer)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const sameAvatar = Bytes.areEqual(oldAvatar, newAvatar);
|
||||||
|
|
||||||
const profileData = {
|
const profileData = {
|
||||||
version: deriveProfileKeyVersion(profileKey, uuid),
|
version: deriveProfileKeyVersion(profileKey, uuid),
|
||||||
name: Bytes.toBase64(bytesName),
|
name: Bytes.toBase64(bytesName),
|
||||||
|
@ -66,7 +69,8 @@ export async function encryptProfileData(
|
||||||
aboutEmoji: bytesAboutEmoji ? Bytes.toBase64(bytesAboutEmoji) : null,
|
aboutEmoji: bytesAboutEmoji ? Bytes.toBase64(bytesAboutEmoji) : null,
|
||||||
badgeIds: (badges || []).map(({ id }) => id),
|
badgeIds: (badges || []).map(({ id }) => id),
|
||||||
paymentAddress: window.storage.get('paymentAddress') || null,
|
paymentAddress: window.storage.get('paymentAddress') || null,
|
||||||
avatar: Boolean(avatarBuffer),
|
avatar: Boolean(newAvatar),
|
||||||
|
sameAvatar,
|
||||||
commitment: deriveProfileKeyCommitment(profileKey, uuid),
|
commitment: deriveProfileKeyCommitment(profileKey, uuid),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7226,13 +7226,6 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2021-05-05T23:11:22.692Z"
|
"updated": "2021-05-05T23:11:22.692Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/AvatarPreview.tsx",
|
|
||||||
"line": " const startingAvatarPathRef = useRef<undefined | string>(",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2021-08-03T21:17:38.615Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/AvatarTextEditor.tsx",
|
"path": "ts/components/AvatarTextEditor.tsx",
|
||||||
|
@ -8079,4 +8072,4 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-09-17T21:02:59.414Z"
|
"updated": "2021-09-17T21:02:59.414Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue