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
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import * as log from '../logging/log';
|
||||
|
@ -46,10 +46,6 @@ export const AvatarPreview = ({
|
|||
onClick,
|
||||
style = {},
|
||||
}: PropsType): JSX.Element => {
|
||||
const startingAvatarPathRef = useRef<undefined | string>(
|
||||
avatarValue ? undefined : avatarPath
|
||||
);
|
||||
|
||||
const [avatarPreview, setAvatarPreview] = useState<Uint8Array | undefined>();
|
||||
|
||||
// Loads the initial avatarPath if one is provided, but only if we're in editable mode.
|
||||
|
@ -60,8 +56,7 @@ export const AvatarPreview = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const startingAvatarPath = startingAvatarPathRef.current;
|
||||
if (!startingAvatarPath) {
|
||||
if (!avatarPath) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
|
@ -69,14 +64,12 @@ export const AvatarPreview = ({
|
|||
|
||||
(async () => {
|
||||
try {
|
||||
const buffer = await imagePathToBytes(startingAvatarPath);
|
||||
const buffer = await imagePathToBytes(avatarPath);
|
||||
if (shouldCancel) {
|
||||
return;
|
||||
}
|
||||
setAvatarPreview(buffer);
|
||||
if (onAvatarLoaded) {
|
||||
onAvatarLoaded(buffer);
|
||||
}
|
||||
onAvatarLoaded?.(buffer);
|
||||
} catch (err) {
|
||||
if (shouldCancel) {
|
||||
return;
|
||||
|
@ -92,7 +85,7 @@ export const AvatarPreview = ({
|
|||
return () => {
|
||||
shouldCancel = true;
|
||||
};
|
||||
}, [onAvatarLoaded, isEditable]);
|
||||
}, [avatarPath, onAvatarLoaded, isEditable]);
|
||||
|
||||
// Ensures that when avatarValue changes we generate new URLs
|
||||
useEffect(() => {
|
||||
|
|
|
@ -25,7 +25,7 @@ const stories = storiesOf('Components/ProfileEditor', module);
|
|||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
aboutEmoji: overrideProps.aboutEmoji,
|
||||
aboutText: text('about', overrideProps.aboutText || ''),
|
||||
avatarPath: overrideProps.avatarPath,
|
||||
profileAvatarPath: overrideProps.profileAvatarPath,
|
||||
clearUsernameSave: action('clearUsernameSave'),
|
||||
conversationId: '123',
|
||||
color: overrideProps.color || getRandomColor(),
|
||||
|
@ -64,7 +64,7 @@ stories.add('Full Set', () => {
|
|||
{...createProps({
|
||||
aboutEmoji: '🙏',
|
||||
aboutText: 'Live. Laugh. Love',
|
||||
avatarPath: '/fixtures/kitten-3-64-64.jpg',
|
||||
profileAvatarPath: '/fixtures/kitten-3-64-64.jpg',
|
||||
onSetSkinTone: setSkinTone,
|
||||
familyName: getLastName(),
|
||||
skinTone,
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { AvatarColorType } from '../types/Colors';
|
|||
import { AvatarColors } from '../types/Colors';
|
||||
import type {
|
||||
AvatarDataType,
|
||||
AvatarUpdateType,
|
||||
DeleteAvatarFromDiskActionType,
|
||||
ReplaceAvatarActionType,
|
||||
SaveAvatarToDiskActionType,
|
||||
|
@ -58,14 +59,14 @@ type PropsExternalType = {
|
|||
onEditStateChanged: (editState: EditState) => unknown;
|
||||
onProfileChanged: (
|
||||
profileData: ProfileDataType,
|
||||
avatarBuffer?: Uint8Array
|
||||
avatar: AvatarUpdateType
|
||||
) => unknown;
|
||||
};
|
||||
|
||||
export type PropsDataType = {
|
||||
aboutEmoji?: string;
|
||||
aboutText?: string;
|
||||
avatarPath?: string;
|
||||
profileAvatarPath?: string;
|
||||
color?: AvatarColorType;
|
||||
conversationId: string;
|
||||
familyName?: string;
|
||||
|
@ -211,7 +212,7 @@ function mapSaveStateToEditState({
|
|||
export const ProfileEditor = ({
|
||||
aboutEmoji,
|
||||
aboutText,
|
||||
avatarPath,
|
||||
profileAvatarPath,
|
||||
clearUsernameSave,
|
||||
color,
|
||||
conversationId,
|
||||
|
@ -254,9 +255,16 @@ export const ProfileEditor = ({
|
|||
UsernameEditState.Editing
|
||||
);
|
||||
|
||||
const [startingAvatarPath, setStartingAvatarPath] =
|
||||
useState(profileAvatarPath);
|
||||
|
||||
const [oldAvatarBuffer, setOldAvatarBuffer] = useState<
|
||||
Uint8Array | undefined
|
||||
>(undefined);
|
||||
const [avatarBuffer, setAvatarBuffer] = useState<Uint8Array | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [isLoadingAvatar, setIsLoadingAvatar] = useState(true);
|
||||
const [stagedProfile, setStagedProfile] = useState<ProfileDataType>({
|
||||
aboutEmoji,
|
||||
aboutText,
|
||||
|
@ -285,6 +293,9 @@ export const ProfileEditor = ({
|
|||
// To make AvatarEditor re-render less often
|
||||
const handleAvatarChanged = useCallback(
|
||||
(avatar: Uint8Array | undefined) => {
|
||||
// Do not display stale avatar from disk anymore.
|
||||
setStartingAvatarPath(undefined);
|
||||
|
||||
setAvatarBuffer(avatar);
|
||||
setEditState(EditState.None);
|
||||
onProfileChanged(
|
||||
|
@ -295,10 +306,11 @@ export const ProfileEditor = ({
|
|||
? trim(stagedProfile.familyName)
|
||||
: undefined,
|
||||
},
|
||||
avatar
|
||||
{ oldAvatar: oldAvatarBuffer, newAvatar: avatar }
|
||||
);
|
||||
setOldAvatarBuffer(avatar);
|
||||
},
|
||||
[onProfileChanged, stagedProfile]
|
||||
[onProfileChanged, stagedProfile, oldAvatarBuffer]
|
||||
);
|
||||
|
||||
const getFullNameText = () => {
|
||||
|
@ -405,9 +417,14 @@ export const ProfileEditor = ({
|
|||
};
|
||||
|
||||
// To make AvatarEditor re-render less often
|
||||
const handleAvatarLoaded = useCallback(avatar => {
|
||||
setAvatarBuffer(avatar);
|
||||
}, []);
|
||||
const handleAvatarLoaded = useCallback(
|
||||
avatar => {
|
||||
setAvatarBuffer(avatar);
|
||||
setOldAvatarBuffer(avatar);
|
||||
setIsLoadingAvatar(false);
|
||||
},
|
||||
[setAvatarBuffer, setOldAvatarBuffer, setIsLoadingAvatar]
|
||||
);
|
||||
|
||||
let content: JSX.Element;
|
||||
|
||||
|
@ -415,7 +432,7 @@ export const ProfileEditor = ({
|
|||
content = (
|
||||
<AvatarEditor
|
||||
avatarColor={color || AvatarColors[0]}
|
||||
avatarPath={avatarPath}
|
||||
avatarPath={startingAvatarPath}
|
||||
avatarValue={avatarBuffer}
|
||||
conversationId={conversationId}
|
||||
conversationTitle={getFullNameText()}
|
||||
|
@ -430,6 +447,7 @@ export const ProfileEditor = ({
|
|||
);
|
||||
} else if (editState === EditState.ProfileName) {
|
||||
const shouldDisableSave =
|
||||
isLoadingAvatar ||
|
||||
!stagedProfile.firstName ||
|
||||
(stagedProfile.firstName === fullName.firstName &&
|
||||
stagedProfile.familyName === fullName.familyName) ||
|
||||
|
@ -502,7 +520,10 @@ export const ProfileEditor = ({
|
|||
familyName: stagedProfile.familyName,
|
||||
});
|
||||
|
||||
onProfileChanged(stagedProfile, avatarBuffer);
|
||||
onProfileChanged(stagedProfile, {
|
||||
oldAvatar: oldAvatarBuffer,
|
||||
newAvatar: avatarBuffer,
|
||||
});
|
||||
handleBack();
|
||||
}}
|
||||
>
|
||||
|
@ -513,8 +534,9 @@ export const ProfileEditor = ({
|
|||
);
|
||||
} else if (editState === EditState.Bio) {
|
||||
const shouldDisableSave =
|
||||
stagedProfile.aboutText === fullBio.aboutText &&
|
||||
stagedProfile.aboutEmoji === fullBio.aboutEmoji;
|
||||
isLoadingAvatar ||
|
||||
(stagedProfile.aboutText === fullBio.aboutText &&
|
||||
stagedProfile.aboutEmoji === fullBio.aboutEmoji);
|
||||
|
||||
content = (
|
||||
<>
|
||||
|
@ -613,7 +635,10 @@ export const ProfileEditor = ({
|
|||
aboutText: stagedProfile.aboutText,
|
||||
});
|
||||
|
||||
onProfileChanged(stagedProfile, avatarBuffer);
|
||||
onProfileChanged(stagedProfile, {
|
||||
oldAvatar: oldAvatarBuffer,
|
||||
newAvatar: avatarBuffer,
|
||||
});
|
||||
handleBack();
|
||||
}}
|
||||
>
|
||||
|
@ -689,7 +714,7 @@ export const ProfileEditor = ({
|
|||
<>
|
||||
<AvatarPreview
|
||||
avatarColor={color}
|
||||
avatarPath={avatarPath}
|
||||
avatarPath={startingAvatarPath}
|
||||
avatarValue={avatarBuffer}
|
||||
conversationTitle={getFullNameText()}
|
||||
i18n={i18n}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ConfirmationDialog } from './ConfirmationDialog';
|
|||
import type { PropsType as ProfileEditorPropsType } from './ProfileEditor';
|
||||
import { ProfileEditor, EditState } from './ProfileEditor';
|
||||
import type { ProfileDataType } from '../state/ducks/conversations';
|
||||
import type { AvatarUpdateType } from '../types/Avatar';
|
||||
|
||||
export type PropsDataType = {
|
||||
hasError: boolean;
|
||||
|
@ -15,7 +16,7 @@ export type PropsDataType = {
|
|||
type PropsType = {
|
||||
myProfileChanged: (
|
||||
profileData: ProfileDataType,
|
||||
avatarBuffer?: Uint8Array
|
||||
avatar: AvatarUpdateType
|
||||
) => unknown;
|
||||
toggleProfileEditor: () => unknown;
|
||||
toggleProfileEditorHasError: () => unknown;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue