From da426ae03b7dc1ee9eca4b096889bf39cefc107d Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 27 Jan 2025 17:57:12 +0400 Subject: [PATCH] Introduce fast-buttons-bots for support mode. --- .../history/history_item_components.cpp | 40 ++++++++- .../history/history_item_components.h | 2 + .../SourceFiles/history/history_widget.cpp | 40 +++++++++ Telegram/SourceFiles/history/history_widget.h | 1 + .../history/view/history_view_message.cpp | 31 +++---- .../info/profile/info_profile_actions.cpp | 33 +++++++- .../SourceFiles/storage/storage_account.cpp | 4 + .../SourceFiles/storage/storage_account.h | 1 + .../SourceFiles/support/support_helper.cpp | 84 +++++++++++++++++++ Telegram/SourceFiles/support/support_helper.h | 26 ++++-- 10 files changed, 238 insertions(+), 24 deletions(-) diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 2fe99415b..c57dfc6f3 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -50,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "window/window_session_controller.h" #include "api/api_bot.h" +#include "support/support_helper.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" #include "styles/style_dialogs.h" // dialogsMiniReplyStory. @@ -880,12 +881,17 @@ void ReplyKeyboard::paint( Assert(_width > 0); _st->startPaint(p, st); + auto number = hasFastButtonMode() ? 1 : 0; for (auto y = 0, rowsCount = int(_rows.size()); y != rowsCount; ++y) { for (auto x = 0, count = int(_rows[y].size()); x != count; ++x) { + const auto guard = gsl::finally([&] { if (number) ++number; }); const auto &button = _rows[y][x]; const auto rect = button.rect; - if (rect.y() >= clip.y() + clip.height()) return; - if (rect.y() + rect.height() < clip.y()) continue; + if (rect.y() >= clip.y() + clip.height()) { + return; + } else if (rect.y() + rect.height() < clip.y()) { + continue; + } // just ignore the buttons that didn't layout well if (rect.x() + rect.width() > _width) break; @@ -904,10 +910,27 @@ void ReplyKeyboard::paint( ? Corner::Large : Corner::Small; _st->paintButton(p, st, outerWidth, button, buttonRounding); + + if (number) { + p.setFont(st::dialogsUnreadFont); + p.setPen(st->msgServiceFg()); + p.drawText( + rect.x() + st::msgBotKbIconPadding, + rect.y() + st::dialogsUnreadFont->ascent, + QString::number(number)); + } } } } +bool ReplyKeyboard::hasFastButtonMode() const { + return _item->inlineReplyKeyboard() + && (_item == _item->history()->lastMessage()) + && _item->history()->session().supportMode() + && _item->history()->session().supportHelper().fastButtonMode( + _item->history()->peer); +} + ClickHandlerPtr ReplyKeyboard::getLink(QPoint point) const { Assert(_width > 0); @@ -927,6 +950,19 @@ ClickHandlerPtr ReplyKeyboard::getLink(QPoint point) const { return ClickHandlerPtr(); } +ClickHandlerPtr ReplyKeyboard::getLinkByIndex(int index) const { + auto number = 1; + for (const auto &row : _rows) { + for (const auto &button : row) { + if (number == index + 1) { + return button.link; + } + ++number; + } + } + return ClickHandlerPtr(); +} + void ReplyKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (!p) return; diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 8b38c294d..c2281496b 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -504,6 +504,7 @@ public: int outerWidth, const QRect &clip) const; ClickHandlerPtr getLink(QPoint point) const; + ClickHandlerPtr getLinkByIndex(int index) const; void clickHandlerActiveChanged( const ClickHandlerPtr &p, @@ -537,6 +538,7 @@ private: }; void startAnimation(int i, int j, int direction); + [[nodiscard]] bool hasFastButtonMode() const; ButtonCoords findButtonCoordsByClickHandler(const ClickHandlerPtr &p); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 7eab91e35..66d3080a1 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -166,6 +166,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/ui_integration.h" #include "support/support_common.h" #include "support/support_autocomplete.h" +#include "support/support_helper.h" #include "support/support_preload.h" #include "dialogs/dialogs_key.h" #include "calls/calls_instance.h" @@ -467,6 +468,8 @@ HistoryWidget::HistoryWidget( }); InitMessageFieldFade(_field, st::historyComposeField.textBg); + setupFastButtonMode(); + _fieldCharsCountManager.limitExceeds( ) | rpl::start_with_next([=] { const auto hide = _fieldCharsCountManager.isLimitExceeded(); @@ -2888,6 +2891,43 @@ void HistoryWidget::refreshSilentToggle() { } } +void HistoryWidget::setupFastButtonMode() { + if (!session().supportMode()) { + return; + } + const auto field = _field->rawTextEdit(); + base::install_event_filter(field, [=](not_null<QEvent*> e) { + if (e->type() != QEvent::KeyPress + || !_history + || !session().supportHelper().fastButtonMode(_history->peer) + || !_field->getLastText().isEmpty()) { + return base::EventFilterResult::Continue; + } + const auto k = static_cast<QKeyEvent*>(e.get()); + const auto key = k->key(); + if (key < Qt::Key_1 || key > Qt::Key_9 || k->modifiers()) { + return base::EventFilterResult::Continue; + } + const auto item = _history ? _history->lastMessage() : nullptr; + const auto markup = item ? item->inlineReplyKeyboard() : nullptr; + const auto link = markup + ? markup->getLinkByIndex(key - Qt::Key_1) + : nullptr; + if (!link) { + return base::EventFilterResult::Continue; + } + const auto id = item->fullId(); + ActivateClickHandler(window(), link, { + Qt::LeftButton, + QVariant::fromValue(ClickHandlerContext{ + .itemId = item->fullId(), + .sessionWindow = base::make_weak(controller()), + }), + }); + return base::EventFilterResult::Cancel; + }); +} + void HistoryWidget::setupScheduledToggle() { controller()->activeChatValue( ) | rpl::map([=](Dialogs::Key key) -> rpl::producer<> { diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index ad9c1b74c..7fa51470f 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -653,6 +653,7 @@ private: [[nodiscard]] bool showRecordButton() const; [[nodiscard]] bool showInlineBotCancel() const; void refreshSilentToggle(); + void setupFastButtonMode(); [[nodiscard]] bool isChoosingTheme() const; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index c28069331..e323f3553 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -55,7 +55,7 @@ const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_"; class KeyboardStyle : public ReplyKeyboard::Style { public: - KeyboardStyle(const style::BotKeyboardButton &st); + KeyboardStyle(const style::BotKeyboardButton &st, Fn<void()> repaint); Images::CornersMaskRef buttonRounding( Ui::BubbleRounding outer, @@ -93,12 +93,16 @@ private: mutable base::flat_map<BubbleRoundingKey, QImage> _cachedBg; mutable base::flat_map<BubbleRoundingKey, QPainterPath> _cachedOutline; mutable std::unique_ptr<Ui::GlareEffect> _glare; + Fn<void()> _repaint; rpl::lifetime _lifetime; }; -KeyboardStyle::KeyboardStyle(const style::BotKeyboardButton &st) -: ReplyKeyboard::Style(st) { +KeyboardStyle::KeyboardStyle( + const style::BotKeyboardButton &st, + Fn<void()> repaint) +: ReplyKeyboard::Style(st) +, _repaint(std::move(repaint)) { style::PaletteChanged( ) | rpl::start_with_next([=] { _cachedBg = {}; @@ -120,15 +124,6 @@ const style::TextStyle &KeyboardStyle::textStyle() const { void KeyboardStyle::repaint(not_null<const HistoryItem*> item) const { item->history()->owner().requestItemRepaint(item); - if (_glare && !_glare->glare.birthTime) { - constexpr auto kTimeout = crl::time(0); - constexpr auto kDuration = crl::time(1100); - _glare->validate( - st::premiumButtonFg->c, - [=] { repaint(item); }, - kTimeout, - kDuration); - } } Images::CornersMaskRef KeyboardStyle::buttonRounding( @@ -306,6 +301,11 @@ void KeyboardStyle::paintButtonLoading( } else { _glare = std::make_unique<Ui::GlareEffect>(); _glare->width = outerWidth; + + constexpr auto kTimeout = crl::time(0); + constexpr auto kDuration = crl::time(1100); + const auto color = st::premiumButtonFg->c; + _glare->validate(color, _repaint, kTimeout, kDuration); } } } @@ -3400,9 +3400,12 @@ void Message::validateInlineKeyboard(HistoryMessageReplyMarkup *markup) { || markup->hiddenBy(data()->media())) { return; } + const auto item = data(); markup->inlineKeyboard = std::make_unique<ReplyKeyboard>( - data(), - std::make_unique<KeyboardStyle>(st::msgBotKbButton)); + item, + std::make_unique<KeyboardStyle>( + st::msgBotKbButton, + [=] { item->history()->owner().requestItemRepaint(item); })); } void Message::validateFromNameText(PeerData *from) const { diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index ffa2472ca..bedf78abf 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -740,8 +740,7 @@ auto AddActionButton( ToggleOn &&toggleOn, Callback &&callback, const style::icon *icon, - const style::SettingsButton &st - = st::infoSharedMediaButton) { + const style::SettingsButton &st = st::infoSharedMediaButton) { auto result = parent->add(object_ptr<Ui::SlideWrap<Ui::SettingsButton>>( parent, object_ptr<Ui::SettingsButton>( @@ -1018,6 +1017,7 @@ private: void addEditContactAction(not_null<UserData*> user); void addDeleteContactAction(not_null<UserData*> user); void addBotCommandActions(not_null<UserData*> user); + void addFastButtonsMode(not_null<UserData*> user); void addReportAction(); void addBlockAction(not_null<UserData*> user); void addLeaveChannelAction(not_null<ChannelData*> channel); @@ -2320,7 +2320,36 @@ void ActionsFiller::addDeleteContactAction(not_null<UserData*> user) { &st::infoIconDelete); } +void ActionsFiller::addFastButtonsMode(not_null<UserData*> user) { + Expects(user->isBot()); + + const auto helper = &user->session().supportHelper(); + const auto button = _wrap->add(object_ptr<Ui::SettingsButton>( + _wrap, + rpl::single(u"Fast buttons mode"_q), + st::infoSharedMediaButton)); + object_ptr<Info::Profile::FloatingIcon>( + button, + st::infoIconMediaBot, + st::infoSharedMediaButtonIconPosition); + + AddSkip(_wrap); + AddDivider(_wrap); + AddSkip(_wrap); + + button->toggleOn(helper->fastButtonModeValue(user)); + button->toggledValue( + ) | rpl::filter([=](bool value) { + return value != helper->fastButtonMode(user); + }) | rpl::start_with_next([=](bool value) { + helper->setFastButtonMode(user, value); + }, button->lifetime()); +} + void ActionsFiller::addBotCommandActions(not_null<UserData*> user) { + if (user->session().supportMode()) { + addFastButtonsMode(user); + } const auto window = _controller->parentController(); const auto findBotCommand = [user](const QString &command) { if (!user->isBot()) { diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index afa677ed4..468c291b3 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -162,6 +162,10 @@ QString Account::tempDirectory() const { return _tempPath; } +QString Account::supportModePath() const { + return _databasePath + u"support"_q; +} + StartResult Account::legacyStart(const QByteArray &passcode) { const auto result = readMapWith(MTP::AuthKeyPtr(), passcode); if (result == ReadMapResult::Failed) { diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index a044b0a74..5ac7194b3 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -76,6 +76,7 @@ public: } [[nodiscard]] QString tempDirectory() const; + [[nodiscard]] QString supportModePath() const; [[nodiscard]] MTP::AuthKeyPtr peekLegacyLocalKey() const { return _localKey; diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp index 286f1fc6c..87a13da1b 100644 --- a/Telegram/SourceFiles/support/support_helper.cpp +++ b/Telegram/SourceFiles/support/support_helper.cpp @@ -28,16 +28,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "lang/lang_keys.h" #include "window/window_session_controller.h" +#include "storage/storage_account.h" #include "storage/storage_media_prepare.h" #include "storage/localimageloader.h" #include "core/launcher.h" #include "core/application.h" #include "core/core_settings.h" +#include "main/main_account.h" #include "main/main_session.h" #include "apiwrap.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" +#include <QtCore/QJsonDocument> +#include <QtCore/QJsonArray> + namespace Main { class Session; } // namespace Main @@ -256,6 +261,12 @@ TimeId OccupiedBySomeoneTill(History *history) { return valid ? result : 0; } +QString FastButtonModeIdsPath(not_null<Main::Session*> session) { + const auto base = session->account().local().supportModePath(); + QDir().mkpath(base); + return base + u"/fast_button_mode_ids.json"_q; +} + } // namespace Helper::Helper(not_null<Main::Session*> session) @@ -472,6 +483,79 @@ UserInfo Helper::infoCurrent(not_null<UserData*> user) const { return (i != end(_userInformation)) ? i->second : UserInfo(); } +void Helper::readFastButtonModeBots() { + _readFastButtonModeBots = true; + + auto f = QFile(FastButtonModeIdsPath(_session)); + if (!f.open(QIODevice::ReadOnly)) { + return; + } + const auto data = f.readAll(); + const auto json = QJsonDocument::fromJson(data); + if (!json.isObject()) { + return; + } + const auto object = json.object(); + const auto array = object.value(u"ids"_q).toArray(); + for (const auto &value : array) { + const auto bareId = value.toString().toULongLong(); + _fastButtonModeBots.emplace(PeerId(bareId)); + } +} + +void Helper::writeFastButtonModeBots() { + auto array = QJsonArray(); + for (const auto &id : _fastButtonModeBots) { + array.append(QString::number(id.value)); + } + auto object = QJsonObject(); + object[u"ids"_q] = array; + auto f = QFile(FastButtonModeIdsPath(_session)); + if (f.open(QIODevice::WriteOnly)) { + f.write(QJsonDocument(object).toJson(QJsonDocument::Indented)); + } +} + +bool Helper::fastButtonMode(not_null<PeerData*> peer) const { + if (!_readFastButtonModeBots) { + const_cast<Helper*>(this)->readFastButtonModeBots(); + } + return _fastButtonModeBots.contains(peer->id); +} + +rpl::producer<bool> Helper::fastButtonModeValue( + not_null<PeerData*> peer) const { + return rpl::single( + fastButtonMode(peer) + ) | rpl::then(_fastButtonModeBotsChanges.events( + ) | rpl::filter([=](PeerId id) { + return (peer->id == id); + }) | rpl::map([=] { + return fastButtonMode(peer); + })); +} + +void Helper::setFastButtonMode(not_null<PeerData*> peer, bool fast) { + if (fast == fastButtonMode(peer)) { + return; + } else if (fast) { + _fastButtonModeBots.emplace(peer->id); + } else { + _fastButtonModeBots.remove(peer->id); + } + if (_fastButtonModeBots.empty()) { + QFile(FastButtonModeIdsPath(_session)).remove(); + } else { + writeFastButtonModeBots(); + } + _fastButtonModeBotsChanges.fire_copy(peer->id); + if (const auto history = peer->owner().history(peer)) { + if (const auto item = history->lastMessage()) { + history->owner().requestItemRepaint(item); + } + } +} + void Helper::editInfo( not_null<Window::SessionController*> controller, not_null<UserData*> user) { diff --git a/Telegram/SourceFiles/support/support_helper.h b/Telegram/SourceFiles/support/support_helper.h index fd46acd38..55c4f3199 100644 --- a/Telegram/SourceFiles/support/support_helper.h +++ b/Telegram/SourceFiles/support/support_helper.h @@ -50,19 +50,26 @@ public: void chatOccupiedUpdated(not_null<History*> history); - bool isOccupiedByMe(History *history) const; - bool isOccupiedBySomeone(History *history) const; + [[nodiscard]] bool isOccupiedByMe(History *history) const; + [[nodiscard]] bool isOccupiedBySomeone(History *history) const; void refreshInfo(not_null<UserData*> user); - rpl::producer<UserInfo> infoValue(not_null<UserData*> user) const; - rpl::producer<QString> infoLabelValue(not_null<UserData*> user) const; - rpl::producer<TextWithEntities> infoTextValue( + [[nodiscard]] rpl::producer<UserInfo> infoValue( not_null<UserData*> user) const; - UserInfo infoCurrent(not_null<UserData*> user) const; + [[nodiscard]] rpl::producer<QString> infoLabelValue( + not_null<UserData*> user) const; + [[nodiscard]] rpl::producer<TextWithEntities> infoTextValue( + not_null<UserData*> user) const; + [[nodiscard]] UserInfo infoCurrent(not_null<UserData*> user) const; void editInfo( not_null<Window::SessionController*> controller, not_null<UserData*> user); + [[nodiscard]] bool fastButtonMode(not_null<PeerData*> peer) const; + [[nodiscard]] rpl::producer<bool> fastButtonModeValue( + not_null<PeerData*> peer) const; + void setFastButtonMode(not_null<PeerData*> peer, bool fast); + Templates &templates(); private: @@ -90,6 +97,9 @@ private: TextWithEntities text, Fn<void(bool success)> done); + void writeFastButtonModeBots(); + void readFastButtonModeBots(); + not_null<Main::Session*> _session; MTP::Sender _api; Templates _templates; @@ -107,6 +117,10 @@ private: base::weak_ptr<Window::SessionController>> _userInfoEditPending; base::flat_map<not_null<UserData*>, SavingInfo> _userInfoSaving; + base::flat_set<PeerId> _fastButtonModeBots; + rpl::event_stream<PeerId> _fastButtonModeBotsChanges; + bool _readFastButtonModeBots = false; + rpl::lifetime _lifetime; };