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, noVoiceMessages: voiceMessagesForbidden,
hasPinnedStories: Boolean(storiesPinnedAvailable), hasPinnedStories: Boolean(storiesPinnedAvailable),
isTranslationDisabled: translationsDisabled, isTranslationDisabled: translationsDisabled,
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }), profilePhoto: profilePhoto instanceof GramJs.Photo ? buildApiPhoto(profilePhoto) : undefined,
...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }), fallbackPhoto: fallbackPhoto instanceof GramJs.Photo ? buildApiPhoto(fallbackPhoto) : undefined,
...(personalPhoto instanceof GramJs.Photo && { personalPhoto: buildApiPhoto(personalPhoto) }), personalPhoto: personalPhoto instanceof GramJs.Photo ? buildApiPhoto(personalPhoto) : undefined,
...(premiumGifts && { premiumGifts: premiumGifts.map((gift) => buildApiPremiumGiftOption(gift)) }), ...(premiumGifts && { premiumGifts: premiumGifts.map((gift) => buildApiPremiumGiftOption(gift)) }),
...(botInfo && { botInfo: buildApiBotInfo(botInfo, userId) }), ...(botInfo && { botInfo: buildApiBotInfo(botInfo, userId) }),
}; };

View File

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

View File

@ -307,7 +307,7 @@ const MediaViewer: FC<StateProps> = ({
chatId={avatarOwner.id} chatId={avatarOwner.id}
isAvatar isAvatar
isFallbackAvatar={isUserId(avatarOwner.id) isFallbackAvatar={isUserId(avatarOwner.id)
&& (avatarOwner as ApiUser).photos?.[mediaId!].id === avatarOwnerFallbackPhoto?.id} && (avatarOwner as ApiUser).photos?.[mediaId!]?.id === avatarOwnerFallbackPhoto?.id}
/> />
) : ( ) : (
<SenderInfo <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 React, { memo, useCallback, useState } from '../../lib/teact/teact';
import type { FC } 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 CropModal from './CropModal';
import styles from './SelectAvatar.module.scss'; import styles from './SelectAvatar.module.scss';
import { openSystemFilesDialog } from '../../util/systemFilesDialog';
type OwnProps = { type OwnProps = {
onChange: (file: File) => void; onChange: (file: File) => void;
@ -18,17 +19,6 @@ const SelectAvatar: FC<OwnProps> = ({
}) => { }) => {
const [selectedFile, setSelectedFile] = useState<File | undefined>(); 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) => { const handleAvatarCrop = useCallback((croppedImg: File) => {
setSelectedFile(undefined); setSelectedFile(undefined);
onChange(croppedImg); onChange(croppedImg);
@ -38,15 +28,19 @@ const SelectAvatar: FC<OwnProps> = ({
setSelectedFile(undefined); 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 ( return (
<> <>
<input <input ref={inputRef} className={styles.input} onClick={handleClick} />
type="file"
onChange={handleSelectFile}
accept="image/png, image/jpeg"
ref={inputRef}
className={styles.input}
/>
<CropModal file={selectedFile} onClose={handleModalClose} onChange={handleAvatarCrop} /> <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 => { addActionHandler('loadFullChat', (global, actions, payload): ActionReturnType => {
const { chatId, force, tabId = getCurrentTabId() } = payload!; const {
chatId, force, tabId = getCurrentTabId(), withPhotos,
} = payload!;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return; return;
} }
const loadChat = async () => {
await loadFullChat(global, actions, chat, tabId);
if (withPhotos) {
actions.loadProfilePhotos({ profileId: chatId });
}
};
if (force) { if (force) {
loadFullChat(global, actions, chat, tabId); void loadChat();
} else { } else {
runDebouncedForLoadFullChat(() => loadFullChat(global, actions, chat, tabId)); runDebouncedForLoadFullChat(loadChat);
} }
}); });
@ -1354,6 +1363,10 @@ addActionHandler('updateChat', async (global, actions, payload): Promise<void> =
global = getGlobal(); global = getGlobal();
global = updateManagementProgress(global, ManagementProgress.Complete, tabId); global = updateManagementProgress(global, ManagementProgress.Complete, tabId);
setGlobal(global); setGlobal(global);
if (photo) {
actions.loadFullChat({ chatId, tabId, withPhotos: true });
}
}); });
addActionHandler('updateChatPhoto', async (global, actions, payload): Promise<void> => { 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 // Explicitly delete the old photo reference
await callApi('deleteProfilePhotos', [photo]); await callApi('deleteProfilePhotos', [photo]);
actions.loadFullChat({ chatId, tabId }); actions.loadFullChat({ chatId, tabId, withPhotos: true });
actions.loadProfilePhotos({ profileId: chatId });
}); });
addActionHandler('deleteChatPhoto', async (global, actions, payload): Promise<void> => { addActionHandler('deleteChatPhoto', async (global, actions, payload): Promise<void> => {
@ -1396,11 +1408,19 @@ addActionHandler('deleteChatPhoto', async (global, actions, payload): Promise<vo
photo: nextPhoto, 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 // Delete references to the old photos
const result = await callApi('deleteProfilePhotos', photosToDelete); const result = await callApi('deleteProfilePhotos', photosToDelete);
if (!result) return; if (!result) return;
actions.loadFullChat({ chatId, tabId }); actions.loadFullChat({ chatId, tabId, withPhotos: true });
actions.loadProfilePhotos({ profileId: chatId });
}); });
addActionHandler('toggleSignatures', (global, actions, payload): ActionReturnType => { 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 { setTimeFormat } from '../../../util/langProvider';
import requestActionTimeout from '../../../util/requestActionTimeout'; import requestActionTimeout from '../../../util/requestActionTimeout';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { selectChat, selectUser, selectTabState } from '../../selectors'; import {
selectChat, selectUser, selectTabState, selectUserFullInfo,
} from '../../selectors';
import { import {
addUsers, addBlockedContact, updateChats, updateUser, removeBlockedContact, replaceSettings, updateNotifySettings, addUsers, addBlockedContact, updateChats, updateUser, removeBlockedContact, replaceSettings, updateNotifySettings,
addNotifyExceptions, updateChat, updateUserFullInfo, addNotifyExceptions, updateChat, updateUserFullInfo,
@ -50,7 +52,6 @@ addActionHandler('updateProfile', async (global, actions, payload): Promise<void
global = getGlobal(); global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id')); global = addUsers(global, buildCollectionByKey(result.users, 'id'));
setGlobal(global); setGlobal(global);
actions.loadProfilePhotos({ profileId: currentUserId });
} }
} }
@ -97,6 +98,10 @@ addActionHandler('updateProfile', async (global, actions, payload): Promise<void
}, },
}, tabId); }, tabId);
setGlobal(global); setGlobal(global);
if (photo) {
actions.loadFullUser({ userId: currentUserId, withPhotos: true });
}
}); });
addActionHandler('updateProfilePhoto', async (global, actions, payload): Promise<void> => { 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 = updateUser(global, currentUserId, { avatarHash: undefined });
global = updateUserFullInfo(global, currentUserId, { profilePhoto: undefined }); global = updateUserFullInfo(global, currentUserId, { profilePhoto: undefined });
setGlobal(global); setGlobal(global);
const result = await callApi('updateProfilePhoto', photo, isFallback); const result = await callApi('updateProfilePhoto', photo, isFallback);
@ -117,28 +123,40 @@ addActionHandler('updateProfilePhoto', async (global, actions, payload): Promise
global = getGlobal(); global = getGlobal();
global = addUsers(global, buildCollectionByKey(users, 'id')); global = addUsers(global, buildCollectionByKey(users, 'id'));
setGlobal(global); setGlobal(global);
actions.loadFullUser({ userId: currentUserId, withPhotos: true });
actions.loadFullUser({ userId: currentUserId });
actions.loadProfilePhotos({ profileId: currentUserId });
}); });
addActionHandler('deleteProfilePhoto', async (global, actions, payload): Promise<void> => { addActionHandler('deleteProfilePhoto', async (global, actions, payload): Promise<void> => {
const { photo } = payload; const { photo } = payload;
const { currentUserId } = global; const { currentUserId } = global;
if (!currentUserId) return; if (!currentUserId) return;
const currentUser = selectChat(global, currentUserId); const currentUser = selectUser(global, currentUserId);
if (!currentUser) return; 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 = updateUser(global, currentUserId, { avatarHash: undefined });
global = updateUserFullInfo(global, currentUserId, { profilePhoto: undefined }); global = updateUserFullInfo(global, currentUserId, { profilePhoto: undefined });
setGlobal(global);
} }
const result = await callApi('deleteProfilePhotos', [photo]); if (fullInfo?.fallbackPhoto?.id === photo.id) {
if (!result) return; global = updateUserFullInfo(global, currentUserId, { fallbackPhoto: undefined });
}
actions.loadFullUser({ userId: currentUserId }); if (fullInfo?.personalPhoto?.id === photo.id) {
actions.loadProfilePhotos({ profileId: currentUserId }); 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> => { addActionHandler('checkUsername', async (global, actions, payload): Promise<void> => {

View File

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

View File

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