From 0decf928f0aa96127325a5d645d6bda09d0d3d7e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sun, 21 Sep 2025 09:55:31 +0300 Subject: [PATCH] Added ability to manage contact photo in edit contact box. --- Telegram/Resources/langs/lang.strings | 4 + .../boxes/peers/edit_contact_box.cpp | 226 +++++++++++++++++- Telegram/SourceFiles/ui/menu_icons.style | 1 + 3 files changed, 230 insertions(+), 1 deletion(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 621256c267..71bc7f77c2 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1642,6 +1642,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_set_photo_for_from_clipboard" = "Set From Clipboard"; "lng_profile_set_photo_for_about" = "You can replace {user}'s photo with another photo that only you will see."; "lng_profile_photo_reset" = "Reset to Original"; +"lng_profile_photo_reset_sure" = "Are you sure you want to reset {user}'s photo to the original?"; "lng_profile_photo_from_clipboard" = "From clipboard"; "lng_profile_suggest_sure" = "You can suggest {user} to set this photo for their Telegram profile."; "lng_profile_suggest_button" = "Suggest"; @@ -4798,6 +4799,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_contact_phone_will_be_shared" = "You can make your phone visible to {user}."; "lng_contact_add_notes" = "Note"; "lng_contact_add_notes_about" = "Notes are only visible to you."; +"lng_suggest_photo_for" = "Suggest Photo for {user}"; +"lng_set_photo_for_user" = "Set Photo for {user}"; +"lng_contact_photo_replace_info" = "You can replace {user}'s photo with another photo that only you will see."; "lng_edit_contact_title" = "Edit contact"; "lng_edit_channel_title" = "Edit channel"; "lng_edit_bot_title" = "Edit bot"; diff --git a/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp index dc9ec0bfe3..9c8f5ea6aa 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp @@ -14,18 +14,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_selector.h" #include "core/ui_integration.h" +#include "data/data_changes.h" #include "data/data_document.h" #include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_stickers.h" +#include "editor/photo_editor_common.h" +#include "editor/photo_editor_layer_widget.h" #include "history/view/controls/history_view_characters_limit.h" #include "info/profile/info_profile_cover.h" +#include "info/userpic/info_userpic_emoji_builder_common.h" +#include "info/userpic/info_userpic_emoji_builder_menu_item.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/settings_common.h" #include "ui/controls/emoji_button_factory.h" #include "ui/controls/emoji_button.h" +#include "ui/controls/userpic_button.h" +#include "ui/boxes/confirm_box.h" #include "ui/text/format_values.h" // Ui::FormatPhone #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" @@ -33,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" #include "ui/wrap/vertical_layout.h" #include "window/window_controller.h" #include "window/window_session_controller.h" @@ -40,6 +49,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat_helpers.h" #include "styles/style_info.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" + +#include +#include namespace { @@ -107,19 +121,28 @@ private: void setupCover(); void setupNameFields(); void setupNotesField(); + void setupPhotoButtons(); void setupWarning(); void setupSharePhoneNumber(); void initNameFields( not_null first, not_null last, bool inverted); + void showPhotoMenu(bool suggest); + void choosePhotoFile(bool suggest); + void processChosenPhoto(QImage &&image, bool suggest); + void processChosenPhotoWithMarkup( + UserpicBuilder::Result &&data, + bool suggest); not_null _box; not_null _window; not_null _user; Ui::Checkbox *_sharePhone = nullptr; Ui::InputField *_notesField = nullptr; + Ui::InputField *_firstNameField = nullptr; base::unique_qptr _emojiPanel; + base::unique_qptr _photoMenu; QString _phone; Fn _focus; Fn _save; @@ -153,6 +176,7 @@ void Controller::setupContent() { setupCover(); setupNameFields(); setupNotesField(); + setupPhotoButtons(); setupWarning(); setupSharePhoneNumber(); } @@ -173,13 +197,14 @@ void Controller::setupCover() { void Controller::setupNameFields() { const auto inverted = langFirstNameGoesSecond(); - const auto first = _box->addRow( + _firstNameField = _box->addRow( object_ptr( _box, st::defaultInputField, tr::lng_signup_firstname(), _user->firstName), st::addContactFieldMargin); + const auto first = _firstNameField; auto preparedLast = object_ptr( _box, st::defaultInputField, @@ -393,6 +418,91 @@ void Controller::setupNotesField() { tr::lng_contact_add_notes_about()); } +void Controller::setupPhotoButtons() { + auto nameValue = _firstNameField + ? rpl::merge( + rpl::single(_firstNameField->getLastText().trimmed()), + _firstNameField->changes() | rpl::map([=] { + return _firstNameField->getLastText().trimmed(); + })) | rpl::map([=](const QString &text) { + return text.isEmpty() ? Ui::kQEllipsis : text; + }) + : rpl::single(_user->shortName()); + const auto inner = _box->verticalLayout(); + Ui::AddSkip(inner); + + const auto suggestButton = Settings::AddButtonWithIcon( + inner, + tr::lng_suggest_photo_for(lt_user, rpl::duplicate(nameValue)), + st::settingsButtonLight, + { &st::menuBlueIconPhotoSuggest }); + suggestButton->setClickedCallback([=] { + showPhotoMenu(true); + }); + + const auto setButton = Settings::AddButtonWithIcon( + inner, + tr::lng_set_photo_for_user(lt_user, rpl::duplicate(nameValue)), + st::settingsButtonLight, + { &st::menuBlueIconPhotoSet }); + setButton->setClickedCallback([=] { + showPhotoMenu(false); + }); + + const auto resetButton = Settings::AddButtonWithIcon( + inner, + tr::lng_profile_photo_reset(), + st::settingsButtonLight, + { nullptr }); + + const auto userpicButton = Ui::CreateChild( + resetButton, + _window, + _user, + Ui::UserpicButton::Role::Custom, + Ui::UserpicButton::Source::NonPersonalIfHasPersonal, + st::restoreUserpicIcon); + userpicButton->setAttribute(Qt::WA_TransparentForMouseEvents); + + resetButton->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + userpicButton->move( + st::settingsButtonLight.iconLeft, + (size.height() - userpicButton->height()) / 2); + }, userpicButton->lifetime()); + + _user->session().changes().peerFlagsValue( + _user, + Data::PeerUpdate::Flag::FullInfo + ) | rpl::map([=] { + return _user->hasPersonalPhoto(); + }) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool hasPersonal) { + resetButton->setVisible(hasPersonal); + }, resetButton->lifetime()); + + resetButton->setVisible(_user->hasPersonalPhoto()); + + resetButton->setClickedCallback([=] { + _window->show(Ui::MakeConfirmBox({ + .text = tr::lng_profile_photo_reset_sure( + tr::now, + lt_user, + _user->shortName()), + .confirmed = [=] { + _window->session().api().peerPhoto().clearPersonal(_user); + }, + .confirmText = tr::lng_profile_photo_reset(tr::now), + })); + }); + + Ui::AddSkip(inner); + + Ui::AddDividerText( + inner, + tr::lng_contact_photo_replace_info(lt_user, std::move(nameValue))); +} + void Controller::setupSharePhoneNumber() { const auto settings = _user->barSettings(); if (!settings @@ -415,6 +525,120 @@ void Controller::setupSharePhoneNumber() { } +void Controller::showPhotoMenu(bool suggest) { + _photoMenu = base::make_unique_q( + _box, + st::popupMenuWithIcons); + + _photoMenu->addAction( + tr::lng_attach_photo(tr::now), + [=] { choosePhotoFile(suggest); }, + &st::menuIconPhoto); + + if (const auto data = QGuiApplication::clipboard()->mimeData()) { + if (data->hasImage()) { + auto callback = [=] { + Editor::PrepareProfilePhoto( + _box, + &_window->window(), + Editor::EditorData{ + .about = (suggest + ? tr::lng_profile_suggest_sure( + tr::now, + lt_user, + Ui::Text::Bold(_user->shortName()), + Ui::Text::WithEntities) + : tr::lng_profile_set_personal_sure( + tr::now, + lt_user, + Ui::Text::Bold(_user->shortName()), + Ui::Text::WithEntities)), + .confirm = (suggest + ? tr::lng_profile_suggest_button(tr::now) + : tr::lng_profile_set_photo_button(tr::now)), + .cropType = Editor::EditorData::CropType::Ellipse, + .keepAspectRatio = true, + }, + [=](QImage &&editedImage) { + processChosenPhoto(std::move(editedImage), suggest); + }, + qvariant_cast(data->imageData())); + }; + _photoMenu->addAction( + tr::lng_profile_photo_from_clipboard(tr::now), + std::move(callback), + &st::menuIconPhoto); + } + } + + UserpicBuilder::AddEmojiBuilderAction( + _window, + _photoMenu.get(), + _window->session().api().peerPhoto().emojiListValue( + Api::PeerPhoto::EmojiListType::Profile), + [=](UserpicBuilder::Result data) { + processChosenPhotoWithMarkup(std::move(data), suggest); + }, + false); + + _photoMenu->popup(QCursor::pos()); +} + +void Controller::choosePhotoFile(bool suggest) { + Editor::PrepareProfilePhotoFromFile( + _box, + &_window->window(), + Editor::EditorData{ + .about = (suggest + ? tr::lng_profile_suggest_sure( + tr::now, + lt_user, + Ui::Text::Bold(_user->shortName()), + Ui::Text::WithEntities) + : tr::lng_profile_set_personal_sure( + tr::now, + lt_user, + Ui::Text::Bold(_user->shortName()), + Ui::Text::WithEntities)), + .confirm = (suggest + ? tr::lng_profile_suggest_button(tr::now) + : tr::lng_profile_set_photo_button(tr::now)), + .cropType = Editor::EditorData::CropType::Ellipse, + .keepAspectRatio = true, + }, + [=](QImage &&image) { + processChosenPhoto(std::move(image), suggest); + }); +} + +void Controller::processChosenPhoto(QImage &&image, bool suggest) { + Api::PeerPhoto::UserPhoto photo{ + .image = base::duplicate(image), + }; + if (suggest) { + _window->session().api().peerPhoto().suggest(_user, std::move(photo)); + _window->showPeerHistory(_user->id); + } else { + _window->session().api().peerPhoto().upload(_user, std::move(photo)); + } +} + +void Controller::processChosenPhotoWithMarkup( + UserpicBuilder::Result &&data, + bool suggest) { + Api::PeerPhoto::UserPhoto photo{ + .image = std::move(data.image), + .markupDocumentId = data.id, + .markupColors = std::move(data.colors), + }; + if (suggest) { + _window->session().api().peerPhoto().suggest(_user, std::move(photo)); + _window->showPeerHistory(_user->id); + } else { + _window->session().api().peerPhoto().upload(_user, std::move(photo)); + } +} + } // namespace void EditContactBox( diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index f39a3c5cd9..13cc30bf68 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -200,6 +200,7 @@ menuIconMuteForAnyTextPosition: point(14px, 9px); menuIconMuteForAnyTextFont: font(8px semibold); menuBlueIconPhotoSet: icon {{ "menu/photo_set", lightButtonFg }}; +menuBlueIconPhotoSuggest: icon {{ "menu/photo_suggest", lightButtonFg }}; menuBlueIconPremium: icon{{ "menu/premium", lightButtonFg }}; menuBlueIconColorNames: icon{{ "settings/premium/features/feature_color_names", lightButtonFg }}; menuBlueIconWallpaper: icon{{ "settings/premium/features/feature_wallpaper", lightButtonFg }};