Profile: Fix avatar update/delete (#3731)

This commit is contained in:
Alexander Zinchuk 2023-09-04 04:05:38 +02:00
parent 03c6e79302
commit 3a56c041cd
8 changed files with 101 additions and 50 deletions

View File

@ -31,9 +31,9 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
noVoiceMessages: voiceMessagesForbidden,
hasPinnedStories: Boolean(storiesPinnedAvailable),
isTranslationDisabled: translationsDisabled,
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }),
...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }),
...(personalPhoto instanceof GramJs.Photo && { personalPhoto: buildApiPhoto(personalPhoto) }),
profilePhoto: profilePhoto instanceof GramJs.Photo ? buildApiPhoto(profilePhoto) : undefined,
fallbackPhoto: fallbackPhoto instanceof GramJs.Photo ? buildApiPhoto(fallbackPhoto) : undefined,
personalPhoto: personalPhoto instanceof GramJs.Photo ? buildApiPhoto(personalPhoto) : undefined,
...(premiumGifts && { premiumGifts: premiumGifts.map((gift) => buildApiPremiumGiftOption(gift)) }),
...(botInfo && { botInfo: buildApiBotInfo(botInfo, userId) }),
};

View File

@ -41,6 +41,7 @@ import Avatar from './Avatar';
import './ProfileInfo.scss';
import styles from './ProfileInfo.module.scss';
import { useStateRef } from '../../hooks/useStateRef';
type OwnProps = {
userId: string;
@ -98,10 +99,10 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
const photos = user?.photos || chat?.photos || MEMO_EMPTY_ARRAY;
const prevMediaId = usePrevious(mediaId);
const prevAvatarOwnerId = usePrevious(avatarOwnerId);
const mediaIdRef = useStateRef(mediaId);
const [hasSlideAnimation, setHasSlideAnimation] = useState(true);
const slideAnimation = hasSlideAnimation
? (lang.isRtl ? 'slideOptimizedRtl' : 'slideOptimized')
: 'none';
// slideOptimized doesn't work well when animation is dynamically disabled
const slideAnimation = hasSlideAnimation ? (lang.isRtl ? 'slideRtl' : 'slide') : 'none';
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
const isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0;
@ -115,9 +116,16 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
}
}, [mediaId, prevMediaId, prevAvatarOwnerId]);
// Reset the current avatar photo to the one selected in Media Viewer if photos have changed
useEffect(() => {
setHasSlideAnimation(false);
setCurrentPhotoIndex(mediaIdRef.current || 0);
}, [mediaIdRef, photos]);
// Deleting the last profile photo may result in an error
useEffect(() => {
if (currentPhotoIndex > photos.length) {
setHasSlideAnimation(false);
setCurrentPhotoIndex(Math.max(0, photos.length - 1));
}
}, [currentPhotoIndex, photos.length]);

View File

@ -307,7 +307,7 @@ const MediaViewer: FC<StateProps> = ({
chatId={avatarOwner.id}
isAvatar
isFallbackAvatar={isUserId(avatarOwner.id)
&& (avatarOwner as ApiUser).photos?.[mediaId!].id === avatarOwnerFallbackPhoto?.id}
&& (avatarOwner as ApiUser).photos?.[mediaId!]?.id === avatarOwnerFallbackPhoto?.id}
/>
) : (
<SenderInfo

View File

@ -1,4 +1,4 @@
import type { ChangeEvent, RefObject } from 'react';
import type { RefObject } from 'react';
import React, { memo, useCallback, useState } from '../../lib/teact/teact';
import type { FC } from '../../lib/teact/teact';
@ -6,6 +6,7 @@ import type { FC } from '../../lib/teact/teact';
import CropModal from './CropModal';
import styles from './SelectAvatar.module.scss';
import { openSystemFilesDialog } from '../../util/systemFilesDialog';
type OwnProps = {
onChange: (file: File) => void;
@ -18,17 +19,6 @@ const SelectAvatar: FC<OwnProps> = ({
}) => {
const [selectedFile, setSelectedFile] = useState<File | undefined>();
function handleSelectFile(event: ChangeEvent<HTMLInputElement>) {
const target = event.target as HTMLInputElement;
if (!target?.files?.[0]) {
return;
}
setSelectedFile(target.files[0]);
target.value = '';
}
const handleAvatarCrop = useCallback((croppedImg: File) => {
setSelectedFile(undefined);
onChange(croppedImg);
@ -38,15 +28,19 @@ const SelectAvatar: FC<OwnProps> = ({
setSelectedFile(undefined);
}, []);
const handleClick = useCallback(() => {
openSystemFilesDialog('image/png, image/jpeg', ((event) => {
const target = event.target as HTMLInputElement;
if (!target?.files?.[0]) {
return;
}
setSelectedFile(target.files[0]);
}), true);
}, []);
return (
<>
<input
type="file"
onChange={handleSelectFile}
accept="image/png, image/jpeg"
ref={inputRef}
className={styles.input}
/>
<input ref={inputRef} className={styles.input} onClick={handleClick} />
<CropModal file={selectedFile} onClose={handleModalClose} onChange={handleAvatarCrop} />
</>
);

View File

@ -343,16 +343,25 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
});
addActionHandler('loadFullChat', (global, actions, payload): ActionReturnType => {
const { chatId, force, tabId = getCurrentTabId() } = payload!;
const {
chatId, force, tabId = getCurrentTabId(), withPhotos,
} = payload!;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const loadChat = async () => {
await loadFullChat(global, actions, chat, tabId);
if (withPhotos) {
actions.loadProfilePhotos({ profileId: chatId });
}
};
if (force) {
loadFullChat(global, actions, chat, tabId);
void loadChat();
} else {
runDebouncedForLoadFullChat(() => loadFullChat(global, actions, chat, tabId));
runDebouncedForLoadFullChat(loadChat);
}
});
@ -1354,6 +1363,10 @@ addActionHandler('updateChat', async (global, actions, payload): Promise<void> =
global = getGlobal();
global = updateManagementProgress(global, ManagementProgress.Complete, tabId);
setGlobal(global);
if (photo) {
actions.loadFullChat({ chatId, tabId, withPhotos: true });
}
});
addActionHandler('updateChatPhoto', async (global, actions, payload): Promise<void> => {
@ -1371,8 +1384,7 @@ addActionHandler('updateChatPhoto', async (global, actions, payload): Promise<vo
});
// Explicitly delete the old photo reference
await callApi('deleteProfilePhotos', [photo]);
actions.loadFullChat({ chatId, tabId });
actions.loadProfilePhotos({ profileId: chatId });
actions.loadFullChat({ chatId, tabId, withPhotos: true });
});
addActionHandler('deleteChatPhoto', async (global, actions, payload): Promise<void> => {
@ -1396,11 +1408,19 @@ addActionHandler('deleteChatPhoto', async (global, actions, payload): Promise<vo
photo: nextPhoto,
});
}
const { photos = [] } = chat;
const newPhotos = photos.filter((p) => photosToDelete.some((toDelete) => toDelete.id !== p.id));
global = getGlobal();
global = updateChat(global, chatId, { photos: newPhotos });
setGlobal(global);
// Delete references to the old photos
const result = await callApi('deleteProfilePhotos', photosToDelete);
if (!result) return;
actions.loadFullChat({ chatId, tabId });
actions.loadProfilePhotos({ profileId: chatId });
actions.loadFullChat({ chatId, tabId, withPhotos: true });
});
addActionHandler('toggleSignatures', (global, actions, payload): ActionReturnType => {

View File

@ -17,7 +17,9 @@ import { subscribe, unsubscribe, requestPermission } from '../../../util/notific
import { setTimeFormat } from '../../../util/langProvider';
import requestActionTimeout from '../../../util/requestActionTimeout';
import { getServerTime } from '../../../util/serverTime';
import { selectChat, selectUser, selectTabState } from '../../selectors';
import {
selectChat, selectUser, selectTabState, selectUserFullInfo,
} from '../../selectors';
import {
addUsers, addBlockedContact, updateChats, updateUser, removeBlockedContact, replaceSettings, updateNotifySettings,
addNotifyExceptions, updateChat, updateUserFullInfo,
@ -50,7 +52,6 @@ addActionHandler('updateProfile', async (global, actions, payload): Promise<void
global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
setGlobal(global);
actions.loadProfilePhotos({ profileId: currentUserId });
}
}
@ -97,6 +98,10 @@ addActionHandler('updateProfile', async (global, actions, payload): Promise<void
},
}, tabId);
setGlobal(global);
if (photo) {
actions.loadFullUser({ userId: currentUserId, withPhotos: true });
}
});
addActionHandler('updateProfilePhoto', async (global, actions, payload): Promise<void> => {
@ -108,6 +113,7 @@ addActionHandler('updateProfilePhoto', async (global, actions, payload): Promise
global = updateUser(global, currentUserId, { avatarHash: undefined });
global = updateUserFullInfo(global, currentUserId, { profilePhoto: undefined });
setGlobal(global);
const result = await callApi('updateProfilePhoto', photo, isFallback);
@ -117,28 +123,40 @@ addActionHandler('updateProfilePhoto', async (global, actions, payload): Promise
global = getGlobal();
global = addUsers(global, buildCollectionByKey(users, 'id'));
setGlobal(global);
actions.loadFullUser({ userId: currentUserId });
actions.loadProfilePhotos({ profileId: currentUserId });
actions.loadFullUser({ userId: currentUserId, withPhotos: true });
});
addActionHandler('deleteProfilePhoto', async (global, actions, payload): Promise<void> => {
const { photo } = payload;
const { currentUserId } = global;
if (!currentUserId) return;
const currentUser = selectChat(global, currentUserId);
const currentUser = selectUser(global, currentUserId);
if (!currentUser) return;
if (currentUser.avatarHash === photo.id) {
const fullInfo = selectUserFullInfo(global, currentUserId);
if (currentUser.avatarHash === photo.id || fullInfo?.profilePhoto?.id === photo.id) {
global = updateUser(global, currentUserId, { avatarHash: undefined });
global = updateUserFullInfo(global, currentUserId, { profilePhoto: undefined });
setGlobal(global);
}
const result = await callApi('deleteProfilePhotos', [photo]);
if (!result) return;
if (fullInfo?.fallbackPhoto?.id === photo.id) {
global = updateUserFullInfo(global, currentUserId, { fallbackPhoto: undefined });
}
actions.loadFullUser({ userId: currentUserId });
actions.loadProfilePhotos({ profileId: currentUserId });
if (fullInfo?.personalPhoto?.id === photo.id) {
global = updateUserFullInfo(global, currentUserId, { personalPhoto: undefined });
}
const { photos = [] } = currentUser;
const newPhotos = photos.filter((p) => p.id !== photo.id);
global = updateUser(global, currentUserId, { photos: newPhotos });
setGlobal(global);
await callApi('deleteProfilePhotos', [photo]);
actions.loadFullUser({ userId: currentUserId, withPhotos: true });
});
addActionHandler('checkUsername', async (global, actions, payload): Promise<void> => {

View File

@ -26,6 +26,7 @@ import {
updateUsers,
updateUserSearch,
updateUserSearchFetchingStatus,
updateUserFullInfo,
} from '../../reducers';
import { getServerTime } from '../../../util/serverTime';
import * as langProvider from '../../../util/langProvider';
@ -36,7 +37,7 @@ const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
const runThrottledForSearch = throttle((cb) => cb(), 500, false);
addActionHandler('loadFullUser', async (global, actions, payload): Promise<void> => {
const { userId } = payload!;
const { userId, withPhotos } = payload!;
const user = selectUser(global, userId);
if (!user) {
return;
@ -53,8 +54,15 @@ addActionHandler('loadFullUser', async (global, actions, payload): Promise<void>
const hasChangedProfilePhoto = fullInfo?.profilePhoto?.id !== newFullInfo?.profilePhoto?.id;
const hasChangedFallbackPhoto = fullInfo?.fallbackPhoto?.id !== newFullInfo?.fallbackPhoto?.id;
const hasChangedPersonalPhoto = fullInfo?.personalPhoto?.id !== newFullInfo?.personalPhoto?.id;
if ((hasChangedAvatarHash || hasChangedProfilePhoto || hasChangedFallbackPhoto || hasChangedPersonalPhoto)
&& user.photos?.length) {
const hasChangedPhoto = hasChangedAvatarHash
|| hasChangedProfilePhoto
|| hasChangedFallbackPhoto
|| hasChangedPersonalPhoto;
global = updateUser(global, userId, result.user);
global = updateUserFullInfo(global, userId, result.fullInfo);
setGlobal(global);
if (withPhotos || (user.photos?.length && hasChangedPhoto)) {
actions.loadProfilePhotos({ profileId: userId });
}
});
@ -271,12 +279,14 @@ addActionHandler('loadProfilePhotos', async (global, actions, payload): Promise<
const userOrChat = user || chat;
const { photos, users } = result;
photos.sort((a) => (a.id === userOrChat?.avatarHash ? -1 : 1));
const fallbackPhoto = fullInfo?.fallbackPhoto;
const personalPhoto = fullInfo?.personalPhoto;
if (fallbackPhoto) photos.push(fallbackPhoto);
if (personalPhoto) photos.unshift(personalPhoto);
photos.sort((a) => (a.id === userOrChat?.avatarHash ? -1 : 1));
global = addUsers(global, buildCollectionByKey(users, 'id'));
if (isPrivate) {

View File

@ -1727,6 +1727,7 @@ export interface ActionPayloads {
} & WithTabId;
loadFullChat: {
chatId: string;
withPhotos?: boolean;
force?: boolean;
} & WithTabId;
updateChatPhoto: {
@ -2095,7 +2096,7 @@ export interface ActionPayloads {
setUserSearchQuery: { query?: string } & WithTabId;
loadCommonChats: WithTabId | undefined;
reportSpam: { chatId: string };
loadFullUser: { userId: string };
loadFullUser: { userId: string; withPhotos?: boolean };
openAddContactDialog: { userId?: string } & WithTabId;
openNewContactDialog: WithTabId | undefined;
closeNewContactDialog: WithTabId | undefined;