diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 719d53a9b..8592512ba 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -12,7 +12,7 @@ import type { ApiChatBannedRights, ApiChatAdminRights, ApiGroupCall, - ApiUserStatus, + ApiUserStatus, ApiPhoto, } from '../../types'; import { @@ -41,7 +41,7 @@ import { isMessageWithMedia, buildChatBannedRights, buildChatAdminRights, - buildInputChatReactions, + buildInputChatReactions, buildInputPhoto, } from '../gramjsBuilders'; import { addEntitiesWithPhotosToLocalDb, addMessageToLocalDb, addPhotoToLocalDb } from '../helpers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; @@ -693,24 +693,33 @@ export async function createGroupChat({ export async function editChatPhoto({ chatId, accessHash, photo, }: { - chatId: string; accessHash?: string; photo: File; + chatId: string; accessHash?: string; photo?: File | ApiPhoto; }) { - const uploadedPhoto = await uploadFile(photo); const inputEntity = buildInputEntity(chatId, accessHash); - + let inputPhoto: GramJs.TypeInputChatPhoto; + if (photo instanceof File) { + const uploadedPhoto = await uploadFile(photo); + inputPhoto = new GramJs.InputChatUploadedPhoto({ + file: uploadedPhoto, + }); + } else if (photo) { + const photoId = buildInputPhoto(photo); + if (!photoId) return false; + inputPhoto = new GramJs.InputChatPhoto({ + id: photoId, + }); + } else { + inputPhoto = new GramJs.InputChatPhotoEmpty(); + } return invokeRequest( inputEntity instanceof GramJs.InputChannel ? new GramJs.channels.EditPhoto({ channel: inputEntity as GramJs.InputChannel, - photo: new GramJs.InputChatUploadedPhoto({ - file: uploadedPhoto, - }), + photo: inputPhoto, }) : new GramJs.messages.EditChatPhoto({ chatId: inputEntity as BigInt.BigInteger, - photo: new GramJs.InputChatUploadedPhoto({ - file: uploadedPhoto, - }), + photo: inputPhoto, }), true, ); diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index afae50dc0..db530f560 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -54,7 +54,7 @@ export { export { updateProfile, checkUsername, updateUsername, fetchBlockedContacts, blockContact, unblockContact, - updateProfilePhoto, uploadProfilePhoto, deleteProfilePhoto, fetchWallpapers, uploadWallpaper, + updateProfilePhoto, uploadProfilePhoto, deleteProfilePhotos, fetchWallpapers, uploadWallpaper, fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations, fetchWebAuthorizations, terminateWebAuthorization, terminateAllWebAuthorizations, fetchNotificationExceptions, fetchNotificationSettings, updateContactSignUpNotification, updateNotificationSettings, diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index 53981c192..a8c73da82 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -21,6 +21,7 @@ import { buildPrivacyRules, } from '../apiBuilders/misc'; +import { buildApiPhoto } from '../apiBuilders/common'; import { buildApiUser } from '../apiBuilders/users'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; @@ -32,7 +33,7 @@ import { import { getClient, invokeRequest, uploadFile } from './client'; import { buildCollectionByKey } from '../../../util/iteratees'; import { getServerTime } from '../../../util/serverTime'; -import { addEntitiesWithPhotosToLocalDb } from '../helpers'; +import { addEntitiesWithPhotosToLocalDb, addPhotoToLocalDb } from '../helpers'; import localDb from '../localDb'; const MAX_INT_32 = 2 ** 31 - 1; @@ -67,26 +68,32 @@ export function updateUsername(username: string) { return invokeRequest(new GramJs.account.UpdateUsername({ username }), true); } -export async function updateProfilePhoto(file: File) { +export async function updateProfilePhoto(photo?: ApiPhoto) { + const photoId = photo ? buildInputPhoto(photo) : new GramJs.InputPhotoEmpty(); + const result = await invokeRequest(new GramJs.photos.UpdateProfilePhoto({ + id: photoId, + })); + if (result?.photo instanceof GramJs.Photo) { + addPhotoToLocalDb(result.photo); + return buildApiPhoto(result.photo); + } + return undefined; +} + +export async function uploadProfilePhoto(file: File) { const inputFile = await uploadFile(file); return invokeRequest(new GramJs.photos.UploadProfilePhoto({ file: inputFile, }), true); } -export async function uploadProfilePhoto(file: File) { - const inputFile = await uploadFile(file); - await invokeRequest(new GramJs.photos.UploadProfilePhoto({ - file: inputFile, - })); -} - -export async function deleteProfilePhoto(photo: ApiPhoto) { - const photoId = buildInputPhoto(photo); - if (!photoId) return false; - const isDeleted = await invokeRequest(new GramJs.photos.DeletePhotos({ id: [photoId] }), true); +export async function deleteProfilePhotos(photos: ApiPhoto[]) { + const photoIds = photos.map(buildInputPhoto).filter(Boolean); + const isDeleted = await invokeRequest(new GramJs.photos.DeletePhotos({ id: photoIds }), true); if (isDeleted) { - delete localDb.photos[photo.id]; + photos.forEach((photo) => { + delete localDb.photos[photo.id]; + }); } return isDeleted; } diff --git a/src/components/common/DeleteProfilePhotoModal.tsx b/src/components/common/DeleteProfilePhotoModal.tsx index b253ae82f..b37286a5e 100644 --- a/src/components/common/DeleteProfilePhotoModal.tsx +++ b/src/components/common/DeleteProfilePhotoModal.tsx @@ -7,6 +7,7 @@ import useLang from '../../hooks/useLang'; import Modal from '../ui/Modal'; import Button from '../ui/Button'; +import { isUserId } from '../../global/helpers'; export type OwnProps = { isOpen: boolean; @@ -25,13 +26,21 @@ const DeleteProfilePhotoModal: FC = ({ }) => { const { deleteProfilePhoto, + deleteChatPhoto, } = getActions(); const handleDeletePhoto = useCallback(() => { onConfirm?.(); - deleteProfilePhoto({ photo, profileId }); + if (isUserId(profileId)) { + deleteProfilePhoto({ photo }); + } else { + deleteChatPhoto({ + photo, + chatId: profileId, + }); + } onClose(); - }, [onConfirm, deleteProfilePhoto, photo, profileId, onClose]); + }, [onConfirm, profileId, onClose, deleteProfilePhoto, photo, deleteChatPhoto]); const lang = useLang(); diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 165280b19..ba6da8582 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -179,7 +179,9 @@ const ProfileInfo: FC = ({ } function renderPhoto(isActive?: boolean) { - const photo = !isSavedMessages && photos.length > 0 ? photos[currentPhotoIndex] : undefined; + const photo = !isSavedMessages && photos.length > 0 + ? photos[currentPhotoIndex] + : undefined; return ( = ({ mediaId, senderId, isChatWithSelf, - canDeleteMedia, + canUpdateMedia, origin, avatarOwner, message, @@ -336,11 +336,12 @@ const MediaViewer: FC = ({ mediaData={bestData} isVideo={isVideo} message={message} - canDeleteAvatar={canDeleteMedia && !!avatarPhoto} + canUpdateMedia={canUpdateMedia} avatarPhoto={avatarPhoto} - avatarOwnerId={avatarOwner?.id} + avatarOwner={avatarOwner} fileName={fileName} canReport={canReport} + selectMedia={selectMedia} onBeforeDelete={handleBeforeDelete} onReport={openReportModal} onCloseMediaViewer={handleClose} @@ -422,10 +423,13 @@ export default memo(withGlobal( if (avatarOwnerId) { const user = selectUser(global, avatarOwnerId); const chat = selectChat(global, avatarOwnerId); - let canDeleteMedia = false; - if (user) canDeleteMedia = avatarOwnerId === currentUserId; - // TODO Support deleting chat photos - // if (chat) canDeleteMedia = isChatAdmin(chat); + let canUpdateMedia = false; + if (user) { + canUpdateMedia = avatarOwnerId === currentUserId; + } else if (chat) { + canUpdateMedia = isChatAdmin(chat); + } + isChatWithSelf = selectIsChatWithSelf(global, avatarOwnerId); return { @@ -433,7 +437,7 @@ export default memo(withGlobal( senderId: avatarOwnerId, avatarOwner: user || chat, isChatWithSelf, - canDeleteMedia, + canUpdateMedia, animationLevel, origin, shouldSkipHistoryAnimations, diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index 55c0badf5..b404f6156 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -7,7 +7,7 @@ import { getActions, withGlobal } from '../../global'; import type { FC } from '../../lib/teact/teact'; import type { - ApiMessage, ApiPhoto, + ApiMessage, ApiPhoto, ApiChat, ApiUser, } from '../../api/types'; import type { MessageListType } from '../../global/types'; import type { MenuItemProps } from '../ui/MenuItem'; @@ -20,7 +20,7 @@ import { selectIsChatProtected, } from '../../global/selectors'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; -import { getMessageMediaFormat, getMessageMediaHash } from '../../global/helpers'; +import { getMessageMediaFormat, getMessageMediaHash, isUserId } from '../../global/helpers'; import useLang from '../../hooks/useLang'; import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress'; @@ -40,7 +40,9 @@ type StateProps = { isProtected?: boolean; isChatProtected?: boolean; canDelete?: boolean; + canUpdate?: boolean; messageListType?: MessageListType; + avatarOwnerId?: string; }; type OwnProps = { @@ -48,11 +50,13 @@ type OwnProps = { isVideo: boolean; zoomLevelChange: number; message?: ApiMessage; - canDeleteAvatar?: boolean; + canUpdateMedia?: boolean; + isSingleMedia?: boolean; avatarPhoto?: ApiPhoto; - avatarOwnerId?: string; + avatarOwner?: ApiChat | ApiUser; fileName?: string; canReport?: boolean; + selectMedia: (mediaId?: number) => void; onReport: NoneToVoidFunction; onBeforeDelete: NoneToVoidFunction; onCloseMediaViewer: NoneToVoidFunction; @@ -73,7 +77,9 @@ const MediaViewerActions: FC = ({ canReport, zoomLevelChange, canDelete, + canUpdate, messageListType, + selectMedia, onReport, onCloseMediaViewer, onBeforeDelete, @@ -85,6 +91,8 @@ const MediaViewerActions: FC = ({ const { downloadMessageMedia, cancelMessageMediaDownload, + updateProfilePhoto, + updateChatPhoto, } = getActions(); const { loadProgress: downloadProgress } = useMediaWithLoadProgress( @@ -111,6 +119,16 @@ const MediaViewerActions: FC = ({ setZoomLevelChange(change + 1); }, [setZoomLevelChange, zoomLevelChange]); + const handleUpdate = useCallback(() => { + if (!avatarPhoto || !avatarOwnerId) return; + if (isUserId(avatarOwnerId)) { + updateProfilePhoto({ photo: avatarPhoto }); + } else { + updateChatPhoto({ chatId: avatarOwnerId, photo: avatarPhoto }); + } + selectMedia(0); + }, [avatarPhoto, avatarOwnerId, selectMedia, updateProfilePhoto, updateChatPhoto]); + const lang = useLang(); const MenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { @@ -217,6 +235,14 @@ const MediaViewerActions: FC = ({ }); } + if (canUpdate) { + menuItems.push({ + icon: 'copy-media', + onClick: handleUpdate, + children: lang('ProfilePhoto.SetMainPhoto'), + }); + } + if (canDelete) { menuItems.push({ icon: 'delete', @@ -298,6 +324,17 @@ const MediaViewerActions: FC = ({ )} + {canUpdate && ( + + )} {canDelete && (