"Set Bot Photo" button in managed bots.

This commit is contained in:
John Preston
2026-03-31 12:59:06 +07:00
parent f62ae325df
commit 95a53d51a3
10 changed files with 170 additions and 23 deletions
+1
View File
@@ -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";
@@ -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;
+2 -1
View File
@@ -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<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }
+6 -2
View File
@@ -565,9 +565,9 @@ not_null<UserData*> 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<UserData*> 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();
+4
View File
@@ -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 {
+8
View File
@@ -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<Data::Forum> _forum;
@@ -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
@@ -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);
}
@@ -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<PeerData*> 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<TextWithEntities> ResolveIsCustom(
not_null<Data::Session*> owner,
DocumentId id) {
@@ -140,6 +154,53 @@ namespace {
return result;
}
[[nodiscard]] object_ptr<Ui::RippleButton> SetupSetBotPhotoButton(
object_ptr<Ui::RippleButton> button) {
const auto raw = button.data();
const auto icon = Ui::CreateChild<Ui::RpWidget>(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<Ui::FlatLabel>(
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<Ui::FlatLabel> _requestChatInfo;
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _emojiStatusInfo;
object_ptr<Ui::PlainShadow> _emojiStatusShadow;
object_ptr<Ui::RippleButton> _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<BgButton>(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<Bar>(parent.get(), peer->shortName()))
, _bar(parent, object_ptr<Bar>::fromRaw(_inner)) {
FinalizeSetBotPhotoFirstOpenState(peer);
setupState(peer, showInForum);
setupHandlers(peer);
}
@@ -585,12 +668,22 @@ auto ContactStatus::PeerState(not_null<PeerData*> 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<PeerData*> 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<PeerData*> peer) {
setupAddHandler(user);
setupBlockHandler(user);
setupShareHandler(user);
setupSetBotPhotoHandler(user);
}
setupUnarchiveHandler(peer);
setupReportHandler(peer);
@@ -785,6 +895,16 @@ void ContactStatus::setupCloseHandler(not_null<PeerData*> 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<PeerData*> peer) {
}, _bar.lifetime());
}
void ContactStatus::setupSetBotPhotoHandler(not_null<UserData*> user) {
_inner->setBotPhotoClicks(
) | rpl::on_next([=] {
_controller->showEditPeerBox(user);
}, _bar.lifetime());
}
void ContactStatus::show() {
if (!_shown) {
_shown = true;
@@ -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<PeerData*> peer);
void setupRequestInfoHandler(not_null<PeerData*> peer);
void setupEmojiStatusHandler(not_null<PeerData*> peer);
void setupSetBotPhotoHandler(not_null<UserData*> user);
static rpl::producer<State> PeerState(not_null<PeerData*> peer);