diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 79870f17d..e59cc1e44 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1200,6 +1200,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_send_message" = "Send Message"; "lng_info_add_as_contact" = "Add to contacts"; "lng_profile_shared_media" = "Shared media"; +"lng_profile_suggest_photo" = "Suggest Photo for {user}"; +"lng_profile_set_photo_for" = "Set Photo for {user}"; +"lng_profile_photo_reset" = "Reset to Original Photo"; "lng_media_type_photos" = "Photos"; "lng_media_type_gifs" = "GIFs"; "lng_media_type_videos" = "Videos"; diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp index be7fcadac..c488c7374 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.cpp +++ b/Telegram/SourceFiles/api/api_peer_photo.cpp @@ -113,6 +113,13 @@ PeerPhoto::PeerPhoto(not_null api) } void PeerPhoto::upload(not_null peer, QImage &&image) { + upload(peer, std::move(image), false); +} + +void PeerPhoto::upload( + not_null peer, + QImage &&image, + bool suggestion) { peer = peer->migrateToOrMe(); const auto ready = PreparePeerPhoto( _api.instance().mainDcId(), @@ -128,12 +135,20 @@ void PeerPhoto::upload(not_null peer, QImage &&image) { [](const auto &pair) { return pair.second; }); if (already != end(_uploads)) { _session->uploader().cancel(already->first); + _suggestions.remove(already->first); _uploads.erase(already); } _uploads.emplace(fakeId, peer); + if (suggestion) { + _suggestions.emplace(fakeId); + } _session->uploader().uploadMedia(fakeId, ready); } +void PeerPhoto::suggest(not_null peer, QImage &&image) { + upload(peer, std::move(image), true); +} + void PeerPhoto::clear(not_null photo) { const auto self = _session->user(); if (self->userpicPhotoId() == photo->id) { @@ -167,6 +182,27 @@ void PeerPhoto::clear(not_null photo) { } } +void PeerPhoto::clearPersonal(not_null user) { + _api.request(MTPphotos_UploadContactProfilePhoto( + MTP_flags(MTPphotos_UploadContactProfilePhoto::Flag::f_save), + user->inputUser, + MTPInputFile(), + MTPInputFile(), // video + MTPdouble() // video_start_ts + )).done([=](const MTPphotos_Photo &result) { + result.match([&](const MTPDphotos_photo &data) { + _session->data().processPhoto(data.vphoto()); + _session->data().processUsers(data.vusers()); + }); + }).send(); + if (!user->userpicPhotoUnknown() + && (user->flags() & UserDataFlag::PersonalPhoto)) { + _session->storage().remove(Storage::UserPhotosRemoveOne( + peerToUser(user->id), + user->userpicPhotoId())); + } +} + void PeerPhoto::set(not_null peer, not_null photo) { if (peer->userpicPhotoId() == photo->id) { return; @@ -200,6 +236,8 @@ void PeerPhoto::set(not_null peer, not_null photo) { void PeerPhoto::ready(const FullMsgId &msgId, const MTPInputFile &file) { const auto maybePeer = _uploads.take(msgId); + const auto suggestion = _suggestions.contains(msgId); + _suggestions.remove(msgId); if (!maybePeer) { return; } @@ -239,6 +277,21 @@ void PeerPhoto::ready(const FullMsgId &msgId, const MTPInputFile &file) { MTPInputFile(), // video MTPdouble()) // video_start_ts )).done(applier).afterRequest(history->sendRequestId).send(); + } else if (const auto user = peer->asUser()) { + using Flag = MTPphotos_UploadContactProfilePhoto::Flag; + _api.request(MTPphotos_UploadContactProfilePhoto( + MTP_flags(Flag::f_file + | (suggestion ? Flag::f_suggest : Flag::f_save)), + user->inputUser, + file, + MTPInputFile(), // video + MTPdouble() // video_start_ts + )).done([=](const MTPphotos_Photo &result) { + result.match([&](const MTPDphotos_photo &data) { + _session->data().processPhoto(data.vphoto()); + _session->data().processUsers(data.vusers()); + }); + }).send(); } } @@ -257,26 +310,35 @@ void PeerPhoto::requestUserPhotos( )).done([this, user](const MTPphotos_Photos &result) { _userPhotosRequests.remove(user); - const auto fullCount = result.match([](const MTPDphotos_photos &d) { + auto fullCount = result.match([](const MTPDphotos_photos &d) { return int(d.vphotos().v.size()); }, [](const MTPDphotos_photosSlice &d) { return d.vcount().v; }); + auto &owner = _session->data(); auto photoIds = result.match([&](const auto &data) { - auto &owner = _session->data(); owner.processUsers(data.vusers()); auto photoIds = std::vector(); photoIds.reserve(data.vphotos().v.size()); - for (const auto &photo : data.vphotos().v) { - if (const auto photoData = owner.processPhoto(photo)) { - photoIds.push_back(photoData->id); + for (const auto &single : data.vphotos().v) { + const auto photo = owner.processPhoto(single); + if (!photo->isNull()) { + photoIds.push_back(photo->id); } } return photoIds; }); + if (!user->userpicPhotoUnknown() + && (user->flags() & UserDataFlag::PersonalPhoto)) { + const auto photo = owner.photo(user->userpicPhotoId()); + if (!photo->isNull()) { + ++fullCount; + photoIds.insert(begin(photoIds), photo->id); + } + } _session->storage().add(Storage::UserPhotosAddSlice( peerToUser(user->id), diff --git a/Telegram/SourceFiles/api/api_peer_photo.h b/Telegram/SourceFiles/api/api_peer_photo.h index ca7b64119..660d965ac 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.h +++ b/Telegram/SourceFiles/api/api_peer_photo.h @@ -25,18 +25,22 @@ public: explicit PeerPhoto(not_null api); void upload(not_null peer, QImage &&image); + void suggest(not_null peer, QImage &&image); void clear(not_null photo); + void clearPersonal(not_null user); void set(not_null peer, not_null photo); void requestUserPhotos(not_null user, UserPhotoId afterId); private: void ready(const FullMsgId &msgId, const MTPInputFile &file); + void upload(not_null peer, QImage &&image, bool suggestion); const not_null _session; MTP::Sender _api; base::flat_map> _uploads; + base::flat_set _suggestions; base::flat_map, mtpRequestId> _userPhotosRequests; diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index c7e88411b..d8ccd86e8 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -168,7 +168,7 @@ TextWithEntities ForumTopicIconWithTitle( return (rootId == ForumTopic::kGeneralId) ? TextWithEntities{ u"# "_q + title } : iconId - ? Data::SingleCustomEmoji(iconId).append(title) + ? Data::SingleCustomEmoji(iconId).append(' ').append(title) : TextWithEntities{ title }; } diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 566a1fd4c..8a55be831 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -423,7 +423,6 @@ private: mutable Data::CloudImage _userpic; PhotoId _userpicPhotoId = kUnknownPhotoId; - bool _userpicHasVideo = false; mutable std::unique_ptr _userpicEmpty; @@ -443,6 +442,7 @@ private: Settings _settings = PeerSettings(PeerSetting::Unknown); BlockStatus _blockStatus = BlockStatus::Unknown; LoadedStatus _loadedStatus = LoadedStatus::Not; + bool _userpicHasVideo = false; QString _requestChatTitle; TimeId _requestChatDate = 0; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 7d3ee694b..7991cc1ae 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -54,11 +54,17 @@ void UserData::setIsContact(bool is) { // see Serialize::readPeer as well void UserData::setPhoto(const MTPUserProfilePhoto &photo) { photo.match([&](const MTPDuserProfilePhoto &data) { + if (data.is_personal()) { + addFlags(UserDataFlag::PersonalPhoto); + } else { + removeFlags(UserDataFlag::PersonalPhoto); + } updateUserpic( data.vphoto_id().v, data.vdc_id().v, data.is_has_video()); }, [&](const MTPDuserProfilePhotoEmpty &) { + removeFlags(UserDataFlag::PersonalPhoto); clearUserpic(); }); } @@ -358,6 +364,9 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { if (const auto photo = update.vprofile_photo()) { user->owner().processPhoto(*photo); } + if (const auto photo = update.vpersonal_photo()) { + user->owner().processPhoto(*photo); + } user->setSettings(update.vsettings()); user->owner().notifySettings().apply(user, update.vnotify_settings()); diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 94c58aadd..ea2bc6e5b 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -56,6 +56,7 @@ enum class UserDataFlag { Premium = (1 << 14), CanReceiveGifts = (1 << 15), VoiceMessagesForbidden = (1 << 16), + PersonalPhoto = (1 << 17), }; inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index a461e3433..cdb435046 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_peer.h" +#include "data/data_user.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_changes.h" @@ -331,12 +332,6 @@ Cover::Cover( setupChildGeometry(); if (_userpic) { - _userpic->uploadPhotoRequests( - ) | rpl::start_with_next([=] { - _peer->session().api().peerPhoto().upload( - _peer, - _userpic->takeResultImage()); - }, _userpic->lifetime()); } else if (topic->canEdit()) { _iconButton->setClickedCallback([=] { _controller->show(Box( @@ -388,30 +383,48 @@ void Cover::initViewers(rpl::producer title) { ) | rpl::start_with_next( [=] { refreshStatusText(); }, lifetime()); - if (!_peer->isUser()) { - _peer->session().changes().peerFlagsValue( - _peer, - Flag::Rights - ) | rpl::start_with_next( - [=] { refreshUploadPhotoOverlay(); }, - lifetime()); - } else if (_peer->isSelf()) { - refreshUploadPhotoOverlay(); - } + _peer->session().changes().peerFlagsValue( + _peer, + (_peer->isUser() ? Flag::IsContact : Flag::Rights) + ) | rpl::start_with_next( + [=] { refreshUploadPhotoOverlay(); }, + lifetime()); } void Cover::refreshUploadPhotoOverlay() { if (!_userpic) { return; } + _userpic->switchChangePhotoOverlay([&] { if (const auto chat = _peer->asChat()) { return chat->canEditInformation(); } else if (const auto channel = _peer->asChannel()) { return channel->canEditInformation(); + } else if (const auto user = _peer->asUser()) { + return user->isSelf() + || (user->isContact() + && !user->isInaccessible() + && !user->isServiceUser()); } - return _peer->isSelf(); - }()); + Unexpected("Peer type in Info::Profile::Cover."); + }(), [=](Ui::UserpicButton::ChosenImage chosen) { + using ChosenType = Ui::UserpicButton::ChosenType; + auto &image = chosen.image; + switch (chosen.type) { + case ChosenType::Set: + _userpic->changeTo(base::duplicate(image)); + _peer->session().api().peerPhoto().upload( + _peer, + std::move(image)); + break; + case ChosenType::Suggest: + _peer->session().api().peerPhoto().suggest( + _peer, + std::move(image)); + break; + } + }); } void Cover::refreshStatusText() { diff --git a/Telegram/SourceFiles/settings/settings_information.cpp b/Telegram/SourceFiles/settings/settings_information.cpp index d6a44f4e1..87810caa3 100644 --- a/Telegram/SourceFiles/settings/settings_information.cpp +++ b/Telegram/SourceFiles/settings/settings_information.cpp @@ -296,9 +296,9 @@ void SetupPhoto( const auto upload = CreateUploadButton(wrap, controller); upload->chosenImages( - ) | rpl::start_with_next([=](QImage &&image) { - UploadPhoto(self, image); - photo->changeTo(std::move(image)); + ) | rpl::start_with_next([=](Ui::UserpicButton::ChosenImage &&chosen) { + UploadPhoto(self, chosen.image); + photo->changeTo(std::move(chosen.image)); }, upload->lifetime()); const auto name = Ui::CreateChild( diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index e23966044..0e2f27df8 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -138,13 +138,12 @@ Cover::Cover( initViewers(); setupChildGeometry(); - _userpic->switchChangePhotoOverlay(_user->isSelf()); - _userpic->uploadPhotoRequests( - ) | rpl::start_with_next([=] { - _user->session().api().peerPhoto().upload( - _user, - _userpic->takeResultImage()); - }, _userpic->lifetime()); + _userpic->switchChangePhotoOverlay(_user->isSelf(), [=]( + Ui::UserpicButton::ChosenImage chosen) { + auto &image = chosen.image; + _userpic->changeTo(base::duplicate(image)); + _user->session().api().peerPhoto().upload(_user, std::move(image)); + }); _badge.setPremiumClickCallback([=] { _emojiStatusPanel.show( diff --git a/Telegram/SourceFiles/ui/special_buttons.cpp b/Telegram/SourceFiles/ui/special_buttons.cpp index c0ee11166..4fa1ba108 100644 --- a/Telegram/SourceFiles/ui/special_buttons.cpp +++ b/Telegram/SourceFiles/ui/special_buttons.cpp @@ -43,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "apiwrap.h" +#include "api/api_peer_photo.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" @@ -246,13 +247,10 @@ void UserpicButton::prepare() { } setClickHandlerByRole(); - if (_role == Role::ChangePhoto || _role == Role::OpenPhoto) { + if (_role == Role::ChangePhoto) { chosenImages( - ) | rpl::start_with_next([=](QImage &&image) { - setImage(std::move(image)); - if (_requestToUpload) { - _uploadPhotoRequests.fire({}); - } + ) | rpl::start_with_next([=](ChosenImage &&chosen) { + setImage(std::move(chosen.image)); }, lifetime()); } } @@ -260,11 +258,8 @@ void UserpicButton::prepare() { void UserpicButton::setClickHandlerByRole() { switch (_role) { case Role::ChoosePhoto: - addClickHandler([=] { choosePhotoLocally(); }); - break; - case Role::ChangePhoto: - addClickHandler([=] { changePhotoLocally(); }); + addClickHandler([=] { choosePhotoLocally(); }); break; case Role::OpenPhoto: @@ -289,10 +284,12 @@ void UserpicButton::choosePhotoLocally() { if (!_window) { return; } - auto callback = [=](QImage &&image) { - _chosenImages.fire(std::move(image)); + const auto callback = [=](ChosenType type) { + return [=](QImage &&image) { + _chosenImages.fire({ std::move(image), type }); + }; }; - const auto chooseFile = [=] { + const auto chooseFile = [=](ChosenType type = ChosenType::Set) { base::call_delayed( _st.changeButton.ripple.hideDuration, crl::guard(this, [=] { @@ -302,32 +299,52 @@ void UserpicButton::choosePhotoLocally() { ((_peer && _peer->isForum()) ? ImageRoundRadius::Large : ImageRoundRadius::Ellipse), - callback); + callback(type)); })); }; if (!IsCameraAvailable()) { chooseFile(); } else { _menu = base::make_unique_q(this); - _menu->addAction(tr::lng_attach_file(tr::now), chooseFile); - _menu->addAction(tr::lng_attach_camera(tr::now), [=] { - _window->show(Box(CameraBox, _window, _peer, callback)); - }); + const auto user = _peer ? _peer->asUser() : nullptr; + if (user && !user->isSelf()) { + const auto name = user->firstName.isEmpty() + ? user->name() + : user->firstName; + _menu->addAction( + tr::lng_profile_set_photo_for(tr::now, lt_user, name), + [=] { chooseFile(); }); + _menu->addAction( + tr::lng_profile_suggest_photo(tr::now, lt_user, name), + [=] { chooseFile(ChosenType::Suggest); }); + if (user->flags() & UserDataFlag::PersonalPhoto) { + _menu->addAction( + tr::lng_profile_photo_reset(tr::now), + [=] { user->session().api().peerPhoto().clearPersonal( + user); _userpicCustom = false; }); + } + } else { + _menu->addAction(tr::lng_attach_file(tr::now), [=] { + chooseFile(); + }); + _menu->addAction(tr::lng_attach_camera(tr::now), [=] { + _window->show(Box( + CameraBox, + _window, + _peer, + callback(ChosenType::Set))); + }); + } _menu->popup(QCursor::pos()); } } -void UserpicButton::changePhotoLocally(bool requestToUpload) { - _requestToUpload = requestToUpload; - choosePhotoLocally(); -} - void UserpicButton::openPeerPhoto() { Expects(_peer != nullptr); Expects(_controller != nullptr); if (_changeOverlayEnabled && _cursorInChangeOverlay) { - changePhotoLocally(true); + choosePhotoLocally(); return; } @@ -720,7 +737,9 @@ void UserpicButton::startAnimation() { _a_appearance.start([this] { update(); }, 0, 1, _st.duration); } -void UserpicButton::switchChangePhotoOverlay(bool enabled) { +void UserpicButton::switchChangePhotoOverlay( + bool enabled, + Fn chosen) { Expects(_role == Role::OpenPhoto); if (_changeOverlayEnabled != enabled) { @@ -731,6 +750,9 @@ void UserpicButton::switchChangePhotoOverlay(bool enabled) { } updateCursorInChangeOverlay( mapFromGlobal(QCursor::pos())); + if (chosen) { + chosenImages() | rpl::start_with_next(chosen, lifetime()); + } } else { _changeOverlayShown.stop(); update(); @@ -791,6 +813,7 @@ void UserpicButton::setImage(QImage &&image) { : Images::Circle(std::move(small))); _userpic.setDevicePixelRatio(cRetinaFactor()); _userpicCustom = _userpicHasImage = true; + _userpicUniqueKey = {}; _result = std::move(image); startNewPhotoShowing(); diff --git a/Telegram/SourceFiles/ui/special_buttons.h b/Telegram/SourceFiles/ui/special_buttons.h index 4a9d95377..d85659ec6 100644 --- a/Telegram/SourceFiles/ui/special_buttons.h +++ b/Telegram/SourceFiles/ui/special_buttons.h @@ -91,18 +91,25 @@ public: Role role, const style::UserpicButton &st); - void switchChangePhotoOverlay(bool enabled); + enum class ChosenType { + Set, + Suggest, + }; + struct ChosenImage { + QImage image; + ChosenType type = ChosenType::Set; + }; + + // Role::OpenPhoto + void switchChangePhotoOverlay( + bool enabled, + Fn chosen); void showSavedMessagesOnSelf(bool enabled); - // Role::ChoosePhoto - [[nodiscard]] rpl::producer chosenImages() const { + // Role::ChoosePhoto or Role::ChangePhoto + [[nodiscard]] rpl::producer chosenImages() const { return _chosenImages.events(); } - - // Role::ChangePhoto - [[nodiscard]] rpl::producer<> uploadPhotoRequests() const { - return _uploadPhotoRequests.events(); - } [[nodiscard]] QImage takeResultImage() { return std::move(_result); } @@ -150,7 +157,6 @@ private: void setClickHandlerByRole(); void openPeerPhoto(); void choosePhotoLocally(); - void changePhotoLocally(bool requestToUpload = false); const style::UserpicButton &_st; ::Window::SessionController *_controller = nullptr; @@ -164,7 +170,6 @@ private: QPixmap _userpic, _oldUserpic; bool _userpicHasImage = false; bool _userpicCustom = false; - bool _requestToUpload = false; InMemoryKey _userpicUniqueKey; Animations::Simple _a_appearance; QImage _result; @@ -181,8 +186,7 @@ private: bool _changeOverlayEnabled = false; Animations::Simple _changeOverlayShown; - rpl::event_stream _chosenImages; - rpl::event_stream<> _uploadPhotoRequests; + rpl::event_stream _chosenImages; };