From 95a53d51a385300569a634d39baed8605cbaeea2 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 31 Mar 2026 12:59:06 +0700 Subject: [PATCH] "Set Bot Photo" button in managed bots. --- Telegram/Resources/langs/lang.strings | 1 + .../chat_helpers/chat_helpers.style | 10 ++ Telegram/SourceFiles/data/data_changes.h | 3 +- Telegram/SourceFiles/data/data_session.cpp | 8 +- Telegram/SourceFiles/data/data_user.cpp | 4 + Telegram/SourceFiles/data/data_user.h | 8 + .../SourceFiles/history/history_widget.cpp | 5 + .../history/view/history_view_about_view.cpp | 15 -- .../view/history_view_contact_status.cpp | 137 +++++++++++++++++- .../view/history_view_contact_status.h | 2 + 10 files changed, 170 insertions(+), 23 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c4f3ed3368..02cb6def36 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2589,6 +2589,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_managed_bot_created_text" = "{parent_name} will manage this bot for you."; "lng_managed_bot_edit_photo" = "You can edit your bot's profile picture {link}"; "lng_managed_bot_edit_photo_link" = "here {arrow}"; +"lng_managed_bot_set_photo" = "Set Profile Photo"; "lng_stake_game_title" = "Emoji Stake"; "lng_stake_game_beta" = "Beta"; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 1adb213892..9f83636345 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -968,6 +968,16 @@ historyEmojiStatusInfoLabel: FlatLabel(historyContactStatusLabel) { } historyContactStatusMinSkip: 16px; +historySetBotPhotoIcon: icon {{ "menu/photo_set", windowActiveTextFg }}; +historySetBotPhotoIconSize: 49px; +historySetBotPhotoLabel: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + textFg: windowActiveTextFg; + align: align(center); + maxHeight: 30px; +} +historySetBotPhotoLabelMarginRight: 20px; + historyBusinessBotPhoto: UserpicButton(defaultUserpicButton) { size: size(46px, 46px); photoSize: 46px; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 84faa0757e..16e14e7b73 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -121,9 +121,10 @@ struct PeerUpdate { ChannelLocation = (1ULL << 53), Slowmode = (1ULL << 54), GroupCall = (1ULL << 55), + ManagedBot = (1ULL << 56), // For iteration - LastUsedBit = (1ULL << 55), + LastUsedBit = (1ULL << 56), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index b3f8e4a921..b8e9c41946 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -565,9 +565,9 @@ not_null Session::processUser(const MTPUser &data) { if (!hasStarsPerMessage) { result->setStarsPerMessage(0); } + result->setBotInfoVersion(data.vbot_info_version().value_or(-1)); if (!minimal) { - result->setBotInfoVersion(data.vbot_info_version().value_or(-1)); if (const auto info = result->botInfo.get()) { info->readsAllHistory = data.is_bot_chat_history(); if (info->cantJoinGroups != data.is_bot_nochats()) { @@ -581,7 +581,11 @@ not_null Session::processUser(const MTPUser &data) { } info->supportsAttachMenu = data.is_bot_attach_menu(); info->supportsBusiness = data.is_bot_business(); - info->canEditInformation = data.is_bot_can_edit(); + const auto canEditInformation = data.is_bot_can_edit(); + if (info->canEditInformation != canEditInformation) { + info->canEditInformation = canEditInformation; + flags |= UpdateFlag::ManagedBot; + } info->activeUsers = data.vbot_active_users().value_or_empty(); info->hasMainApp = data.is_bot_has_main_app(); info->userCreatesTopics = data.is_bot_forum_can_manage_topics(); diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 0300436a0e..be07c2d980 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -323,7 +323,11 @@ UserId UserData::botManagerId() const { } void UserData::setBotManagerId(UserId managerId) { + const auto changed = (_botManagerId != managerId); _botManagerId = managerId; + if (changed) { + session().changes().peerUpdated(this, UpdateFlag::ManagedBot); + } } MTPInputUser UserData::inputUser() const { diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index bde10b135b..7ae58edd3f 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -55,6 +55,12 @@ struct BotVerifierSettings { }; struct BotInfo { + enum class SetBotPhotoOpenState : uchar { + Unknown, + OpenedWithHistory, + OpenedEmpty, + }; + BotInfo(); ~BotInfo(); @@ -90,6 +96,7 @@ struct BotInfo { int version = 0; int descriptionVersion = 0; int activeUsers = 0; + SetBotPhotoOpenState setBotPhotoOpenState = SetBotPhotoOpenState::Unknown; bool inited : 1 = false; bool readsAllHistory : 1 = false; bool cantJoinGroups : 1 = false; @@ -99,6 +106,7 @@ struct BotInfo { bool supportsBusiness : 1 = false; bool hasMainApp : 1 = false; bool userCreatesTopics : 1 = false; + bool setBotPhotoHidden : 1 = false; private: std::unique_ptr _forum; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index fc64b74d96..bd4b4f59f3 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -849,6 +849,7 @@ HistoryWidget::HistoryWidget( | PeerUpdateFlag::MessagesTTL | PeerUpdateFlag::ChatThemeToken | PeerUpdateFlag::FullInfo + | PeerUpdateFlag::ManagedBot | PeerUpdateFlag::StarsPerMessage | PeerUpdateFlag::GiftSettings ) | rpl::filter([=](const Data::PeerUpdate &update) { @@ -909,6 +910,10 @@ HistoryWidget::HistoryWidget( if (flags & PeerUpdateFlag::Slowmode) { updateSendButtonType(); } + if ((flags & PeerUpdateFlag::ManagedBot) && _list) { + _list->refreshAboutView(); + _list->updateBotInfo(); + } if (flags & (PeerUpdateFlag::IsBlocked | PeerUpdateFlag::Admins | PeerUpdateFlag::Members diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 262bab087f..07a126eec0 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -1189,21 +1189,6 @@ AdminLog::OwnedItem AboutView::makeManagedBotInfo( tr::bold(parentName), tr::rich); - const auto url = u"internal:edit_peer/"_q - + QString::number(user->id.value); - text.append(u"\n\n"_q).append(tr::lng_managed_bot_edit_photo( - tr::now, - lt_link, - Ui::Text::Wrapped( - tr::lng_managed_bot_edit_photo_link( - tr::now, - lt_arrow, - Ui::Text::IconEmoji(&st::textMoreIconEmoji), - tr::rich), - EntityType::CustomUrl, - url), - tr::rich)); - return makeAboutSimple(text, nullptr, photo); } diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index fc609e005d..db7accde10 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/message_field.h" // PaidSendButtonText #include "core/click_handler_types.h" #include "core/ui_integration.h" +#include "history/history.h" #include "data/business/data_business_chatbots.h" #include "data/notify/data_notify_settings.h" #include "data/data_emoji_statuses.h" @@ -75,6 +76,19 @@ namespace { return false; } +void FinalizeSetBotPhotoFirstOpenState(not_null peer) { + using State = BotInfo::SetBotPhotoOpenState; + + const auto user = peer->asUser(); + const auto info = user ? user->botInfo.get() : nullptr; + if (!info || (info->setBotPhotoOpenState != State::Unknown)) { + return; + } + info->setBotPhotoOpenState = peer->owner().history(peer)->lastMessage() + ? State::OpenedWithHistory + : State::OpenedEmpty; +} + [[nodiscard]] rpl::producer ResolveIsCustom( not_null owner, DocumentId id) { @@ -140,6 +154,53 @@ namespace { return result; } +[[nodiscard]] object_ptr SetupSetBotPhotoButton( + object_ptr button) { + const auto raw = button.data(); + + const auto icon = Ui::CreateChild(raw); + icon->setAttribute(Qt::WA_TransparentForMouseEvents); + icon->paintRequest( + ) | rpl::on_next([=] { + auto p = QPainter(icon); + st::historySetBotPhotoIcon.paintInCenter(p, icon->rect()); + }, icon->lifetime()); + + const auto label = Ui::CreateChild( + raw, + tr::lng_managed_bot_set_photo(tr::now), + st::historySetBotPhotoLabel); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + raw->sizeValue( + ) | rpl::on_next([=](QSize size) { + const auto closeWidth = st::historyReplyCancel.width; + const auto iconWidth = st::historySetBotPhotoIconSize; + const auto margin = st::historySetBotPhotoLabelMarginRight; + const auto available = size.width() - closeWidth - margin; + const auto labelNatural = label->naturalWidth(); + const auto labelWidth = std::max( + 0, + std::min(labelNatural, available - iconWidth)); + const auto totalContent = iconWidth + labelWidth; + const auto contentLeft = std::max(0, (available - totalContent) / 2); + icon->setGeometry( + contentLeft, + 0, + iconWidth, + size.height()); + label->resizeToWidth(labelWidth); + label->moveToLeft( + contentLeft + iconWidth, + (size.height() - label->height()) / 2, + size.width()); + icon->raise(); + label->raise(); + }, raw->lifetime()); + + return button; +} + } // namespace class ContactStatus::BgButton final : public Ui::RippleButton { @@ -173,6 +234,7 @@ public: [[nodiscard]] rpl::producer<> closeClicks() const; [[nodiscard]] rpl::producer<> requestInfoClicks() const; [[nodiscard]] rpl::producer<> emojiStatusClicks() const; + [[nodiscard]] rpl::producer<> setBotPhotoClicks() const; private: int resizeGetHeight(int newWidth) override; @@ -192,6 +254,7 @@ private: object_ptr _requestChatInfo; object_ptr> _emojiStatusInfo; object_ptr _emojiStatusShadow; + object_ptr _setBotPhoto; bool _emojiStatusRepaintScheduled = false; bool _narrow = false; rpl::event_stream<> _emojiStatusClicks; @@ -260,7 +323,10 @@ ContactStatus::Bar::Bar( st::topBarArrowPadding.top(), st::historyContactStatusMinSkip, st::topBarArrowPadding.top())) -, _emojiStatusShadow(this) { +, _emojiStatusShadow(this) +, _setBotPhoto( + SetupSetBotPhotoButton( + object_ptr(this, st::historyContactStatusButton))) { _close->setAccessibleName(tr::lng_cancel(tr::now)); _unarchiveIcon->setAccessibleName(tr::lng_new_contact_unarchive(tr::now)); _reportIcon->setAccessibleName(tr::lng_report_spam(tr::now)); @@ -286,13 +352,16 @@ void ContactStatus::Bar::showState( _block->setVisible(type == Type::AddOrBlock || type == Type::UnarchiveOrBlock); _share->setVisible(type == Type::SharePhoneNumber); - _close->setVisible(!_narrow && type != Type::RequestChatInfo); + _close->setVisible( + (!_narrow && type != Type::RequestChatInfo) + || type == Type::SetBotPhoto); const auto report = (type == Type::ReportSpam) || (type == Type::UnarchiveOrReport); _report->setVisible(!_narrow && report); _reportIcon->setVisible(_narrow && report); _requestChatInfo->setVisible(type == Type::RequestChatInfo); _requestChatBg->setVisible(type == Type::RequestChatInfo); + _setBotPhoto->setVisible(type == Type::SetBotPhoto); const auto has = !status.empty(); _emojiStatusShadow->setVisible( has && (type == Type::AddOrBlock || type == Type::UnarchiveOrBlock)); @@ -373,12 +442,18 @@ rpl::producer<> ContactStatus::Bar::emojiStatusClicks() const { return _emojiStatusClicks.events(); } +rpl::producer<> ContactStatus::Bar::setBotPhotoClicks() const { + return _setBotPhoto->clicks() | rpl::to_empty; +} + int ContactStatus::Bar::resizeGetHeight(int newWidth) { _close->moveToRight(0, 0, newWidth); const auto narrow = (newWidth < _close->width() * 2); if (_narrow != narrow) { _narrow = narrow; - _close->setVisible(_requestChatInfo->isHidden() && !_narrow); + _close->setVisible( + (_requestChatInfo->isHidden() && !_narrow) + || !_setBotPhoto->isHidden()); const auto report = !_report->isHidden() || !_reportIcon->isHidden(); _report->setVisible(!_narrow && report); _reportIcon->setVisible(_narrow && report); @@ -388,6 +463,13 @@ int ContactStatus::Bar::resizeGetHeight(int newWidth) { _unarchiveIcon->setVisible(_narrow && unarchive); } + if (!_setBotPhoto->isHidden()) { + const auto closeHeight = _close->height(); + _setBotPhoto->setGeometry(0, 0, newWidth, closeHeight); + _close->raise(); + return closeHeight; + } + if (!_unarchiveIcon->isHidden()) { const auto half = newWidth / 2; _unarchiveIcon->setGeometry(0, 0, half, _close->height()); @@ -568,6 +650,7 @@ ContactStatus::ContactStatus( : _controller(window) , _inner(Ui::CreateChild(parent.get(), peer->shortName())) , _bar(parent, object_ptr::fromRaw(_inner)) { + FinalizeSetBotPhotoFirstOpenState(peer); setupState(peer, showInForum); setupHandlers(peer); } @@ -585,12 +668,22 @@ auto ContactStatus::PeerState(not_null peer) return flags.diff & (Flag::Contact | Flag::MutualContact | Flag::Blocked); }); + auto managedBotChanges = user->session().changes().peerFlagsValue( + user, + Data::PeerUpdate::Flag::ManagedBot); + auto photoChanges = user->session().changes().peerFlagsValue( + user, + Data::PeerUpdate::Flag::Photo); return rpl::combine( std::move(changes), - user->barSettingsValue() + user->barSettingsValue(), + std::move(photoChanges), + std::move(managedBotChanges) ) | rpl::map([=]( FlagsChange flags, - SettingsChange settings) -> State { + SettingsChange settings, + const auto &, + const auto &) -> State { if (flags.value & Flag::Blocked) { return { Type::None }; } else if (user->isContact()) { @@ -613,6 +706,22 @@ auto ContactStatus::PeerState(not_null peer) return { Type::AddOrBlock }; } else if (settings.value & PeerBarSetting::AddContact) { return { Type::Add }; + } else if (user->botManagerId()) { + if (const auto info = user->botInfo.get()) { + if (info->canEditInformation) { + const auto hasUserpic = user->hasUserpic(); + if (hasUserpic) { + info->setBotPhotoHidden = true; + } + using State = BotInfo::SetBotPhotoOpenState; + if (info->setBotPhotoOpenState == State::OpenedEmpty + && !info->setBotPhotoHidden + && !hasUserpic) { + return { Type::SetBotPhoto }; + } + } + } + return { Type::None }; } else { return { Type::None }; } @@ -665,6 +774,7 @@ void ContactStatus::setupHandlers(not_null peer) { setupAddHandler(user); setupBlockHandler(user); setupShareHandler(user); + setupSetBotPhotoHandler(user); } setupUnarchiveHandler(peer); setupReportHandler(peer); @@ -785,6 +895,16 @@ void ContactStatus::setupCloseHandler(not_null peer) { ) | rpl::filter([=] { return !(*request); }) | rpl::on_next([=] { + if (_state.type == State::Type::SetBotPhoto) { + if (const auto user = peer->asUser()) { + if (const auto info = user->botInfo.get()) { + info->setBotPhotoHidden = true; + } + } + _state = {}; + _bar.toggleContent(false); + return; + } peer->setBarSettings(0); *request = peer->session().api().request( MTPmessages_HidePeerSettingsBar(peer->input()) @@ -836,6 +956,13 @@ void ContactStatus::setupEmojiStatusHandler(not_null peer) { }, _bar.lifetime()); } +void ContactStatus::setupSetBotPhotoHandler(not_null user) { + _inner->setBotPhotoClicks( + ) | rpl::on_next([=] { + _controller->showEditPeerBox(user); + }, _bar.lifetime()); +} + void ContactStatus::show() { if (!_shown) { _shown = true; diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.h b/Telegram/SourceFiles/history/view/history_view_contact_status.h index 47aa1232ce..b9250747d5 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.h +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.h @@ -93,6 +93,7 @@ private: UnarchiveOrReport, SharePhoneNumber, RequestChatInfo, + SetBotPhoto, }; Type type = Type::None; int starsPerMessage = 0; @@ -111,6 +112,7 @@ private: void setupCloseHandler(not_null peer); void setupRequestInfoHandler(not_null peer); void setupEmojiStatusHandler(not_null peer); + void setupSetBotPhotoHandler(not_null user); static rpl::producer PeerState(not_null peer);