Set / suggest / reset a contact personal photo.

This commit is contained in:
John Preston 2022-12-09 16:34:34 +04:00
parent cb99d611f3
commit 5fe9c93cb6
12 changed files with 190 additions and 72 deletions

View File

@ -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";

View File

@ -113,6 +113,13 @@ PeerPhoto::PeerPhoto(not_null<ApiWrap*> api)
}
void PeerPhoto::upload(not_null<PeerData*> peer, QImage &&image) {
upload(peer, std::move(image), false);
}
void PeerPhoto::upload(
not_null<PeerData*> peer,
QImage &&image,
bool suggestion) {
peer = peer->migrateToOrMe();
const auto ready = PreparePeerPhoto(
_api.instance().mainDcId(),
@ -128,12 +135,20 @@ void PeerPhoto::upload(not_null<PeerData*> 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<PeerData*> peer, QImage &&image) {
upload(peer, std::move(image), true);
}
void PeerPhoto::clear(not_null<PhotoData*> photo) {
const auto self = _session->user();
if (self->userpicPhotoId() == photo->id) {
@ -167,6 +182,27 @@ void PeerPhoto::clear(not_null<PhotoData*> photo) {
}
}
void PeerPhoto::clearPersonal(not_null<UserData*> 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<PeerData*> peer, not_null<PhotoData*> photo) {
if (peer->userpicPhotoId() == photo->id) {
return;
@ -200,6 +236,8 @@ void PeerPhoto::set(not_null<PeerData*> peer, not_null<PhotoData*> 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<PhotoId>();
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),

View File

@ -25,18 +25,22 @@ public:
explicit PeerPhoto(not_null<ApiWrap*> api);
void upload(not_null<PeerData*> peer, QImage &&image);
void suggest(not_null<PeerData*> peer, QImage &&image);
void clear(not_null<PhotoData*> photo);
void clearPersonal(not_null<UserData*> user);
void set(not_null<PeerData*> peer, not_null<PhotoData*> photo);
void requestUserPhotos(not_null<UserData*> user, UserPhotoId afterId);
private:
void ready(const FullMsgId &msgId, const MTPInputFile &file);
void upload(not_null<PeerData*> peer, QImage &&image, bool suggestion);
const not_null<Main::Session*> _session;
MTP::Sender _api;
base::flat_map<FullMsgId, not_null<PeerData*>> _uploads;
base::flat_set<FullMsgId> _suggestions;
base::flat_map<not_null<UserData*>, mtpRequestId> _userPhotosRequests;

View File

@ -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 };
}

View File

@ -423,7 +423,6 @@ private:
mutable Data::CloudImage _userpic;
PhotoId _userpicPhotoId = kUnknownPhotoId;
bool _userpicHasVideo = false;
mutable std::unique_ptr<Ui::EmptyUserpic> _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;

View File

@ -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<UserData*> 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());

View File

@ -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<UserDataFlag>;

View File

@ -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<QString> 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() {

View File

@ -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<Ui::FlatLabel>(

View File

@ -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(

View File

@ -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<Ui::PopupMenu>(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<void(ChosenImage)> 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();

View File

@ -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<void(ChosenImage)> chosen);
void showSavedMessagesOnSelf(bool enabled);
// Role::ChoosePhoto
[[nodiscard]] rpl::producer<QImage> chosenImages() const {
// Role::ChoosePhoto or Role::ChangePhoto
[[nodiscard]] rpl::producer<ChosenImage> 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<QImage> _chosenImages;
rpl::event_stream<> _uploadPhotoRequests;
rpl::event_stream<ChosenImage> _chosenImages;
};