diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 696e84e74..87bbe11e1 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -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) }), }; diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 882d6c4e0..ae63d6bef 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -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 = ({ 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 = ({ } }, [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]); diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index f939e0427..f65a48cde 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -307,7 +307,7 @@ const MediaViewer: FC = ({ chatId={avatarOwner.id} isAvatar isFallbackAvatar={isUserId(avatarOwner.id) - && (avatarOwner as ApiUser).photos?.[mediaId!].id === avatarOwnerFallbackPhoto?.id} + && (avatarOwner as ApiUser).photos?.[mediaId!]?.id === avatarOwnerFallbackPhoto?.id} /> ) : ( void; @@ -18,17 +19,6 @@ const SelectAvatar: FC = ({ }) => { const [selectedFile, setSelectedFile] = useState(); - function handleSelectFile(event: ChangeEvent) { - 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 = ({ 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 ( <> - + ); diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index f0a5a39e5..6e51c74e7 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -343,16 +343,25 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise }); 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 = 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 => { @@ -1371,8 +1384,7 @@ addActionHandler('updateChatPhoto', async (global, actions, payload): Promise => { @@ -1396,11 +1408,19 @@ addActionHandler('deleteChatPhoto', async (global, actions, payload): Promise 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 => { diff --git a/src/global/actions/api/settings.ts b/src/global/actions/api/settings.ts index 8122b9fd5..a5a92cca4 100644 --- a/src/global/actions/api/settings.ts +++ b/src/global/actions/api/settings.ts @@ -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 => { @@ -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 => { 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 => { diff --git a/src/global/actions/api/users.ts b/src/global/actions/api/users.ts index cd7e25cdb..d2be3a615 100644 --- a/src/global/actions/api/users.ts +++ b/src/global/actions/api/users.ts @@ -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 => { - 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 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) { diff --git a/src/global/types.ts b/src/global/types.ts index c4af6c04c..7384da973 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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;