diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index fedb32957..64bbb68d8 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1448,6 +1448,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_info_topic_title" = "Topic Info"; "lng_profile_enable_notifications" = "Notifications"; "lng_profile_send_message" = "Send Message"; +"lng_profile_open_app" = "Open App"; +"lng_profile_open_app_about" = "By launching this mini app, you agree to the {terms}."; +"lng_profile_open_app_terms" = "Terms of Service for Mini Apps"; "lng_info_add_as_contact" = "Add to contacts"; "lng_profile_shared_media" = "Shared media"; "lng_profile_suggest_photo" = "Suggest Profile Photo"; @@ -3186,6 +3189,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_add_to_side_menu_done" = "Bot added to the main menu."; "lng_bot_no_scan_qr" = "QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps."; "lng_bot_click_to_start" = "Click here to use this bot."; +"lng_bot_status_users#one" = "{count} user"; +"lng_bot_status_users#other" = "{count} users"; "lng_typing" = "typing"; "lng_user_typing" = "{user} is typing"; @@ -5341,12 +5346,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_recent_none" = "Recent search results\nwill appear here."; "lng_recent_chats" = "Chats"; "lng_recent_channels" = "Channels"; +"lng_recent_apps" = "Apps"; "lng_channels_none_title" = "No channels yet..."; "lng_channels_none_about" = "You are not currently subscribed to any channels."; "lng_channels_your_title" = "Channels you joined"; "lng_channels_your_more" = "Show more"; "lng_channels_your_less" = "Show less"; "lng_channels_recommended" = "Recommended channels"; +"lng_bot_apps_your" = "Apps you use"; +"lng_bot_apps_popular" = "Popular apps"; "lng_font_box_title" = "Choose font family"; "lng_font_default" = "Default"; diff --git a/Telegram/SourceFiles/data/components/top_peers.cpp b/Telegram/SourceFiles/data/components/top_peers.cpp index f476869b6..57974febf 100644 --- a/Telegram/SourceFiles/data/components/top_peers.cpp +++ b/Telegram/SourceFiles/data/components/top_peers.cpp @@ -41,12 +41,36 @@ constexpr auto kRequestTimeLimit = 10 * crl::time(1000); ) / 1'000'000.; } +[[nodiscard]] MTPTopPeerCategory TypeToCategory(TopPeerType type) { + switch (type) { + case TopPeerType::Chat: return MTP_topPeerCategoryCorrespondents(); + case TopPeerType::BotApp: return MTP_topPeerCategoryBotsApp(); + } + Unexpected("Type in TypeToCategory."); +} + +[[nodiscard]] auto TypeToGetFlags(TopPeerType type) { + using Flag = MTPcontacts_GetTopPeers::Flag; + switch (type) { + case TopPeerType::Chat: return Flag::f_correspondents; + case TopPeerType::BotApp: return Flag::f_bots_app; + } + Unexpected("Type in TypeToGetFlags."); +} + } // namespace -TopPeers::TopPeers(not_null session) -: _session(session) { +TopPeers::TopPeers(not_null session, TopPeerType type) +: _session(session) +, _type(type) { + if (_type == TopPeerType::Chat) { + loadAfterChats(); + } +} + +void TopPeers::loadAfterChats() { using namespace rpl::mappers; - crl::on_main(session, [=] { + crl::on_main(_session, [=] { _session->data().chatsListLoadedEvents( ) | rpl::filter(_1 == nullptr) | rpl::start_with_next([=] { crl::on_main(_session, [=] { @@ -84,7 +108,7 @@ void TopPeers::remove(not_null peer) { } _requestId = _session->api().request(MTPcontacts_ResetTopPeerRating( - MTP_topPeerCategoryCorrespondents(), + TypeToCategory(_type), peer->input )).send(); } @@ -160,11 +184,13 @@ void TopPeers::request() { } _requestId = _session->api().request(MTPcontacts_GetTopPeers( - MTP_flags(MTPcontacts_GetTopPeers::Flag::f_correspondents), + MTP_flags(TypeToGetFlags(_type)), MTP_int(0), MTP_int(kLimit), MTP_long(countHash()) - )).done([=](const MTPcontacts_TopPeers &result, const MTP::Response &response) { + )).done([=]( + const MTPcontacts_TopPeers &result, + const MTP::Response &response) { _lastReceivedDate = TimeId(response.outerMsgId >> 32); _lastReceived = crl::now(); _requestId = 0; @@ -176,19 +202,22 @@ void TopPeers::request() { owner->processChats(data.vchats()); for (const auto &category : data.vcategories().v) { const auto &data = category.data(); - data.vcategory().match( - [&](const MTPDtopPeerCategoryCorrespondents &) { - _list = ranges::views::all( - data.vpeers().v - ) | ranges::views::transform([&](const MTPTopPeer &top) { - return TopPeer{ - owner->peer(peerFromMTP(top.data().vpeer())), - top.data().vrating().v, - }; - }) | ranges::to_vector; - }, [](const auto &) { + const auto cons = (_type == TopPeerType::Chat) + ? mtpc_topPeerCategoryCorrespondents + : mtpc_topPeerCategoryBotsApp; + if (data.vcategory().type() != cons) { LOG(("API Error: Unexpected top peer category.")); - }); + continue; + } + _list = ranges::views::all( + data.vpeers().v + ) | ranges::views::transform([&]( + const MTPTopPeer &top) { + return TopPeer{ + owner->peer(peerFromMTP(top.data().vpeer())), + top.data().vrating().v, + }; + }) | ranges::to_vector; } updated(); }, [&](const MTPDcontacts_topPeersDisabled &) { diff --git a/Telegram/SourceFiles/data/components/top_peers.h b/Telegram/SourceFiles/data/components/top_peers.h index 5f1250b53..7f834ad84 100644 --- a/Telegram/SourceFiles/data/components/top_peers.h +++ b/Telegram/SourceFiles/data/components/top_peers.h @@ -13,9 +13,14 @@ class Session; namespace Data { +enum class TopPeerType { + Chat, + BotApp, +}; + class TopPeers final { public: - explicit TopPeers(not_null session); + TopPeers(not_null session, TopPeerType type); ~TopPeers(); [[nodiscard]] std::vector> list() const; @@ -36,11 +41,13 @@ private: float64 rating = 0.; }; + void loadAfterChats(); void request(); [[nodiscard]] uint64 countHash() const; void updated(); const not_null _session; + const TopPeerType _type = {}; std::vector _list; rpl::event_stream<> _updates; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index cf772208b..f88fb6abb 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -721,6 +721,8 @@ not_null Session::processUser(const MTPUser &data) { result->botInfo->supportsAttachMenu = data.is_bot_attach_menu(); result->botInfo->supportsBusiness = data.is_bot_business(); result->botInfo->canEditInformation = data.is_bot_can_edit(); + result->botInfo->activeUsers = data.vbot_active_users().value_or_empty(); + result->botInfo->hasMainApp = data.is_bot_has_main_app(); } else { result->setBotInfoVersion(-1); } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 67da9e3f5..8f84555d3 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -40,12 +40,14 @@ struct BotInfo { int version = 0; int descriptionVersion = 0; + int activeUsers = 0; bool inited : 1 = false; bool readsAllHistory : 1 = false; bool cantJoinGroups : 1 = false; bool supportsAttachMenu : 1 = false; bool canEditInformation : 1 = false; bool supportsBusiness : 1 = false; + bool hasMainApp : 1 = false; }; enum class UserDataFlag : uint32 { diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 922b673a0..2e7568fc0 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -78,6 +78,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_stories.h" #include "info/downloads/info_downloads_widget.h" #include "info/info_memento.h" +#include "inline_bots/bot_attach_web_view.h" #include "styles/style_dialogs.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" @@ -1265,6 +1266,27 @@ void Widget::updateSuggestions(anim::type animated) { } }, _suggestions->lifetime()); + _suggestions->recentAppChosen( + ) | rpl::start_with_next([=](not_null peer) { + if (const auto user = peer->asUser()) { + if (const auto info = user->botInfo.get()) { + if (info->hasMainApp) { + openBotMainApp(user); + return; + } + } + } + chosenRow({ + .key = peer->owner().history(peer), + .newWindow = base::IsCtrlPressed(), + }); + }, _suggestions->lifetime()); + + _suggestions->popularAppChosen( + ) | rpl::start_with_next([=](not_null peer) { + controller()->showPeerInfo(peer); + }, _suggestions->lifetime()); + updateControlsGeometry(); _suggestions->show(animated, [=] { @@ -1276,6 +1298,17 @@ void Widget::updateSuggestions(anim::type animated) { } } +void Widget::openBotMainApp(not_null bot) { + session().attachWebView().open({ + .bot = bot, + .context = { + .controller = controller(), + .maySkipConfirmation = true, + }, + .source = InlineBots::WebViewSourceBotProfile(), + }); +} + void Widget::changeOpenedSubsection( FnMut change, bool fromRight, diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 347683d0c..a1ee3950d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -214,6 +214,7 @@ private: void refreshTopBars(); void showSearchInTopBar(anim::type animated); void checkUpdateStatus(); + void openBotMainApp(not_null bot); void changeOpenedSubsection( FnMut change, bool fromRight, diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index 84b3f8b89..18ae94b37 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "dialogs/ui/chat_search_empty.h" #include "history/history.h" +#include "inline_bots/bot_attach_web_view.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_common.h" @@ -56,6 +57,8 @@ namespace { constexpr auto kCollapsedChannelsCount = 5; constexpr auto kProbablyMaxChannels = 1000; constexpr auto kProbablyMaxRecommendations = 100; +constexpr auto kCollapsedAppsCount = 5; +constexpr auto kProbablyMaxApps = 100; class RecentRow final : public PeerListRow { public: @@ -315,6 +318,11 @@ public: return _chosen.events(); } + Main::Session &session() const override { + return _window->session(); + } + + void rowClicked(not_null row) override; bool rowTrackPress(not_null row) override; void rowTrackPressCancel() override; bool rowTrackPressSkipMouseSelection() override; @@ -325,7 +333,12 @@ public: protected: [[nodiscard]] int countCurrent() const; void setCount(int count); - void choose(not_null peer); + + [[nodiscard]] bool expandedCurrent() const; + [[nodiscard]] rpl::producer expanded() const; + + void setupPlainDivider(rpl::producer title); + void setupExpandDivider(rpl::producer title); private: const not_null _window; @@ -334,6 +347,8 @@ private: rpl::event_stream<> _touchCancelRequests; rpl::event_stream> _chosen; rpl::variable _count; + rpl::variable _toggleExpanded = nullptr; + rpl::variable _expanded = false; }; @@ -344,11 +359,9 @@ public: RecentPeersList list); void prepare() override; - void rowClicked(not_null row) override; base::unique_qptr rowContextMenu( QWidget *parent, not_null row) override; - Main::Session &session() const override; QString savedMessagesChatStatus() const override; @@ -369,20 +382,15 @@ public: not_null window); void prepare() override; - void rowClicked(not_null row) override; base::unique_qptr rowContextMenu( QWidget *parent, not_null row) override; - Main::Session &session() const override; private: - void setupDivider(); void appendRow(not_null channel); void fill(bool force = false); std::vector> _channels; - rpl::variable _toggleExpanded = nullptr; - rpl::variable _expanded = false; rpl::lifetime _lifetime; }; @@ -394,17 +402,11 @@ public: not_null window); void prepare() override; - void rowClicked(not_null row) override; - base::unique_qptr rowContextMenu( - QWidget *parent, - not_null row) override; - Main::Session &session() const override; void load(); private: void fill(); - void setupDivider(); void appendRow(not_null channel); History *_activeHistory = nullptr; @@ -413,6 +415,45 @@ private: }; +class RecentAppsController final + : public Suggestions::ObjectListController { +public: + explicit RecentAppsController( + not_null window); + + void prepare() override; + + void load(); + +private: + void appendRow(not_null bot); + void fill(); + + std::vector> _bots; + rpl::lifetime _lifetime; + +}; + +class PopularAppsController final + : public Suggestions::ObjectListController { +public: + explicit PopularAppsController( + not_null window); + + void prepare() override; + + void load(); + +private: + void fill(); + void appendRow(not_null bot); + + History *_activeHistory = nullptr; + bool _requested = false; + rpl::lifetime _lifetime; + +}; + Suggestions::ObjectListController::ObjectListController( not_null window) : _window(window) { @@ -510,8 +551,108 @@ void Suggestions::ObjectListController::setCount(int count) { _count = count; } -void Suggestions::ObjectListController::choose(not_null peer) { - _chosen.fire_copy(peer); +bool Suggestions::ObjectListController::expandedCurrent() const { + return _expanded.current(); +} + +rpl::producer Suggestions::ObjectListController::expanded() const { + return _expanded.value(); +} + +void Suggestions::ObjectListController::rowClicked( + not_null row) { + _chosen.fire(row->peer()); +} + +void Suggestions::ObjectListController::setupPlainDivider( + rpl::producer title) { + auto result = object_ptr( + (QWidget*)nullptr, + st::searchedBarHeight); + const auto raw = result.data(); + const auto label = Ui::CreateChild( + raw, + std::move(title), + st::searchedBarLabel); + raw->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto x = st::searchedBarPosition.x(); + const auto y = st::searchedBarPosition.y(); + label->resizeToWidth(size.width() - x * 2); + label->moveToLeft(x, y, size.width()); + }, raw->lifetime()); + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(raw).fillRect(clip, st::searchedBarBg); + }, raw->lifetime()); + + delegate()->peerListSetAboveWidget(std::move(result)); +} + +void Suggestions::ObjectListController::setupExpandDivider( + rpl::producer title) { + auto result = object_ptr( + (QWidget*)nullptr, + st::searchedBarHeight); + const auto raw = result.data(); + const auto label = Ui::CreateChild( + raw, + std::move(title), + st::searchedBarLabel); + count( + ) | rpl::map( + rpl::mappers::_1 > kCollapsedChannelsCount + ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) { + _expanded = false; + if (!more) { + const auto toggle = _toggleExpanded.current(); + _toggleExpanded = nullptr; + delete toggle; + return; + } else if (_toggleExpanded.current()) { + return; + } + const auto toggle = Ui::CreateChild( + raw, + tr::lng_channels_your_more(tr::now), + st::searchedBarLink); + toggle->show(); + toggle->setClickedCallback([=] { + const auto expand = !_expanded.current(); + toggle->setText(expand + ? tr::lng_channels_your_less(tr::now) + : tr::lng_channels_your_more(tr::now)); + _expanded = expand; + }); + rpl::combine( + raw->sizeValue(), + toggle->widthValue() + ) | rpl::start_with_next([=](QSize size, int width) { + const auto x = st::searchedBarPosition.x(); + const auto y = st::searchedBarPosition.y(); + toggle->moveToRight(0, 0, size.width()); + label->resizeToWidth(size.width() - x - width); + label->moveToLeft(x, y, size.width()); + }, toggle->lifetime()); + _toggleExpanded = toggle; + }, raw->lifetime()); + + rpl::combine( + raw->sizeValue(), + _toggleExpanded.value() + ) | rpl::filter( + rpl::mappers::_2 == nullptr + ) | rpl::start_with_next([=](QSize size, const auto) { + const auto x = st::searchedBarPosition.x(); + const auto y = st::searchedBarPosition.y(); + label->resizeToWidth(size.width() - x * 2); + label->moveToLeft(x, y, size.width()); + }, raw->lifetime()); + + raw->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(raw).fillRect(clip, st::searchedBarBg); + }, raw->lifetime()); + + delegate()->peerListSetAboveWidget(std::move(result)); } RecentsController::RecentsController( @@ -533,10 +674,6 @@ void RecentsController::prepare() { subscribeToEvents(); } -void RecentsController::rowClicked(not_null row) { - choose(row->peer()); -} - Fn RecentsController::removeAllCallback() { const auto weak = base::make_weak(this); const auto session = &this->session(); @@ -584,10 +721,6 @@ base::unique_qptr RecentsController::rowContextMenu( return result; } -Main::Session &RecentsController::session() const { - return window()->session(); -} - QString RecentsController::savedMessagesChatStatus() const { return tr::lng_saved_forward_here(tr::now); } @@ -669,7 +802,7 @@ MyChannelsController::MyChannelsController( } void MyChannelsController::prepare() { - setupDivider(); + setupExpandDivider(tr::lng_channels_your_title()); session().changes().peerUpdates( Data::PeerUpdate::Flag::ChannelAmIn @@ -711,7 +844,7 @@ void MyChannelsController::prepare() { ranges::sort(_channels, ranges::greater(), &History::chatListTimeId); setCount(_channels.size()); - _expanded.value() | rpl::start_with_next([=] { + expanded() | rpl::start_with_next([=] { fill(); }, _lifetime); @@ -744,7 +877,7 @@ void MyChannelsController::prepare() { void MyChannelsController::fill(bool force) { const auto count = countCurrent(); - const auto limit = _expanded.current() + const auto limit = expandedCurrent() ? count : std::min(count, kCollapsedChannelsCount); const auto already = delegate()->peerListFullRowsCount(); @@ -776,10 +909,6 @@ void MyChannelsController::appendRow(not_null channel) { delegate()->peerListAppendRow(std::move(row)); } -void MyChannelsController::rowClicked(not_null row) { - choose(row->peer()); -} - base::unique_qptr MyChannelsController::rowContextMenu( QWidget *parent, not_null row) { @@ -798,83 +927,13 @@ base::unique_qptr MyChannelsController::rowContextMenu( return result; } -Main::Session &MyChannelsController::session() const { - return window()->session(); -} - -void MyChannelsController::setupDivider() { - auto result = object_ptr( - (QWidget*)nullptr, - st::searchedBarHeight); - const auto raw = result.data(); - const auto label = Ui::CreateChild( - raw, - tr::lng_channels_your_title(), - st::searchedBarLabel); - count( - ) | rpl::map( - rpl::mappers::_1 > kCollapsedChannelsCount - ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) { - _expanded = false; - if (!more) { - const auto toggle = _toggleExpanded.current(); - _toggleExpanded = nullptr; - delete toggle; - return; - } else if (_toggleExpanded.current()) { - return; - } - const auto toggle = Ui::CreateChild( - raw, - tr::lng_channels_your_more(tr::now), - st::searchedBarLink); - toggle->show(); - toggle->setClickedCallback([=] { - const auto expand = !_expanded.current(); - toggle->setText(expand - ? tr::lng_channels_your_less(tr::now) - : tr::lng_channels_your_more(tr::now)); - _expanded = expand; - }); - rpl::combine( - raw->sizeValue(), - toggle->widthValue() - ) | rpl::start_with_next([=](QSize size, int width) { - const auto x = st::searchedBarPosition.x(); - const auto y = st::searchedBarPosition.y(); - toggle->moveToRight(0, 0, size.width()); - label->resizeToWidth(size.width() - x - width); - label->moveToLeft(x, y, size.width()); - }, toggle->lifetime()); - _toggleExpanded = toggle; - }, raw->lifetime()); - - rpl::combine( - raw->sizeValue(), - _toggleExpanded.value() - ) | rpl::filter( - rpl::mappers::_2 == nullptr - ) | rpl::start_with_next([=](QSize size, const auto) { - const auto x = st::searchedBarPosition.x(); - const auto y = st::searchedBarPosition.y(); - label->resizeToWidth(size.width() - x * 2); - label->moveToLeft(x, y, size.width()); - }, raw->lifetime()); - - raw->paintRequest() | rpl::start_with_next([=](QRect clip) { - QPainter(raw).fillRect(clip, st::searchedBarBg); - }, raw->lifetime()); - - delegate()->peerListSetAboveWidget(std::move(result)); -} - RecommendationsController::RecommendationsController( not_null window) : ObjectListController(window) { } void RecommendationsController::prepare() { - setupDivider(); + setupPlainDivider(tr::lng_channels_recommended()); fill(); } @@ -940,41 +999,115 @@ void RecommendationsController::appendRow(not_null channel) { delegate()->peerListAppendRow(std::move(row)); } -void RecommendationsController::rowClicked(not_null row) { - choose(row->peer()); +RecentAppsController::RecentAppsController( + not_null window) +: ObjectListController(window) { } -base::unique_qptr RecommendationsController::rowContextMenu( - QWidget *parent, - not_null row) { - return nullptr; +void RecentAppsController::prepare() { + setupExpandDivider(tr::lng_bot_apps_your()); + + _bots.reserve(kProbablyMaxApps); + rpl::single() | rpl::then( + session().topBotApps().updates() + ) | rpl::start_with_next([=] { + _bots.clear(); + for (const auto &peer : session().topBotApps().list()) { + if (const auto bot = peer->asUser()) { + if (bot->isBot() && !bot->isInaccessible()) { + _bots.push_back(bot); + } + } + } + setCount(_bots.size()); + while (delegate()->peerListFullRowsCount()) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(0)); + } + fill(); + }, _lifetime); + + expanded() | rpl::skip(1) | rpl::start_with_next([=] { + fill(); + }, _lifetime); } -Main::Session &RecommendationsController::session() const { - return window()->session(); +void RecentAppsController::load() { + session().topBotApps().reload(); } -void RecommendationsController::setupDivider() { - auto result = object_ptr( - (QWidget*)nullptr, - st::searchedBarHeight); - const auto raw = result.data(); - const auto label = Ui::CreateChild( - raw, - tr::lng_channels_recommended(), - st::searchedBarLabel); - raw->sizeValue( - ) | rpl::start_with_next([=](QSize size) { - const auto x = st::searchedBarPosition.x(); - const auto y = st::searchedBarPosition.y(); - label->resizeToWidth(size.width() - x * 2); - label->moveToLeft(x, y, size.width()); - }, raw->lifetime()); - raw->paintRequest() | rpl::start_with_next([=](QRect clip) { - QPainter(raw).fillRect(clip, st::searchedBarBg); - }, raw->lifetime()); +void RecentAppsController::fill() { + const auto count = countCurrent(); + const auto limit = expandedCurrent() + ? count + : std::min(count, kCollapsedAppsCount); + const auto already = delegate()->peerListFullRowsCount(); + const auto delta = limit - already; + if (!delta) { + return; + } else if (delta > 0) { + for (auto i = already; i != limit; ++i) { + appendRow(_bots[i]); + } + } else if (delta < 0) { + for (auto i = already; i != limit;) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(--i)); + } + } + delegate()->peerListRefreshRows(); +} - delegate()->peerListSetAboveWidget(std::move(result)); +void RecentAppsController::appendRow(not_null bot) { + auto row = std::make_unique(bot); + if (const auto count = bot->botInfo->activeUsers) { + row->setCustomStatus( + tr::lng_bot_status_users(tr::now, lt_count_decimal, count)); + } + delegate()->peerListAppendRow(std::move(row)); +} + +PopularAppsController::PopularAppsController( + not_null window) +: ObjectListController(window) { +} + +void PopularAppsController::prepare() { + setupPlainDivider(tr::lng_bot_apps_popular()); + fill(); +} + +void PopularAppsController::load() { + if (_requested || countCurrent()) { + return; + } + _requested = true; + const auto attachWebView = &session().attachWebView(); + attachWebView->loadPopularAppBots(); + attachWebView->popularAppBotsLoaded( + ) | rpl::take(1) | rpl::start_with_next([=] { + fill(); + }, _lifetime); +} + +void PopularAppsController::fill() { + const auto attachWebView = &session().attachWebView(); + const auto &list = attachWebView->popularAppBots(); + if (list.empty()) { + return; + } + for (const auto &bot : list) { + appendRow(bot); + } + delegate()->peerListRefreshRows(); + setCount(delegate()->peerListFullRowsCount()); +} + +void PopularAppsController::appendRow(not_null bot) { + auto row = std::make_unique(bot); + if (const auto count = bot->botInfo->activeUsers) { + row->setCustomStatus( + tr::lng_bot_status_users(tr::now, lt_count_decimal, count)); + } + delegate()->peerListAppendRow(std::move(row)); } Suggestions::Suggestions( @@ -1000,11 +1133,17 @@ Suggestions::Suggestions( _channelsScroll->setOwnedWidget(object_ptr(this))) , _myChannels(setupMyChannels()) , _recommendations(setupRecommendations()) -, _emptyChannels(_channelsContent->add(setupEmptyChannels())) { +, _emptyChannels(_channelsContent->add(setupEmptyChannels())) +, _appsScroll(std::make_unique(this)) +, _appsContent( + _appsScroll->setOwnedWidget(object_ptr(this))) +, _recentApps(setupRecentApps()) +, _popularApps(setupPopularApps()) { setupTabs(); setupChats(); setupChannels(); + setupApps(); } Suggestions::~Suggestions() = default; @@ -1027,10 +1166,15 @@ void Suggestions::setupTabs() { _tabs->setSections({ tr::lng_recent_chats(tr::now), tr::lng_recent_channels(tr::now), + tr::lng_recent_apps(tr::now), }); _tabs->sectionActivated( ) | rpl::start_with_next([=](int section) { - switchTab(section ? Tab::Channels : Tab::Chats); + switchTab(section == 2 + ? Tab::Apps + : section + ? Tab::Channels + : Tab::Chats); }, _tabs->lifetime()); } @@ -1141,12 +1285,30 @@ void Suggestions::setupChannels() { }); } +void Suggestions::setupApps() { + _recentApps->count.value() | rpl::start_with_next([=](int count) { + _recentApps->wrap->toggle(count > 0, anim::type::instant); + }, _recentApps->wrap->lifetime()); + + _popularApps->count.value() | rpl::start_with_next([=](int count) { + _popularApps->wrap->toggle(count > 0, anim::type::instant); + }, _popularApps->wrap->lifetime()); + + _appsScroll->setVisible(_tab.current() == Tab::Apps); + _appsScroll->setCustomTouchProcess([=](not_null e) { + const auto recentApps = _recentApps->processTouch(e); + const auto popularApps = _popularApps->processTouch(e); + return recentApps || popularApps; + }); +} + void Suggestions::selectJump(Qt::Key direction, int pageSize) { - if (_tab.current() == Tab::Chats) { - selectJumpChats(direction, pageSize); - } else { - selectJumpChannels(direction, pageSize); + switch (_tab.current()) { + case Tab::Chats: selectJumpChats(direction, pageSize); return; + case Tab::Channels: selectJumpChannels(direction, pageSize); return; + case Tab::Apps: selectJumpApps(direction, pageSize); return; } + Unexpected("Tab in Suggestions::selectJump."); } void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) { @@ -1260,9 +1422,86 @@ void Suggestions::selectJumpChannels(Qt::Key direction, int pageSize) { } } +void Suggestions::selectJumpApps(Qt::Key direction, int pageSize) { + const auto recentAppsHasSelection = [=] { + return _recentApps->selectJump({}, 0) == JumpResult::Applied; + }; + const auto popularAppsHasSelection = [=] { + return _popularApps->selectJump({}, 0) == JumpResult::Applied; + }; + if (pageSize) { + if (direction == Qt::Key_Down) { + if (popularAppsHasSelection()) { + _popularApps->selectJump(direction, pageSize); + } else if (recentAppsHasSelection()) { + if (_recentApps->selectJump(direction, pageSize) + == JumpResult::AppliedAndOut) { + _popularApps->selectJump(direction, 0); + } + } else if (_recentApps->count.current()) { + _recentApps->selectJump(direction, 0); + _recentApps->selectJump(direction, pageSize); + } else if (_popularApps->count.current()) { + _popularApps->selectJump(direction, 0); + _popularApps->selectJump(direction, pageSize); + } + } else if (direction == Qt::Key_Up) { + if (recentAppsHasSelection()) { + if (_recentApps->selectJump(direction, pageSize) + == JumpResult::AppliedAndOut) { + _channelsScroll->scrollTo(0); + } + } else if (popularAppsHasSelection()) { + if (_popularApps->selectJump(direction, pageSize) + == JumpResult::AppliedAndOut) { + _recentApps->selectJump(direction, -1); + } + } + } + } else if (direction == Qt::Key_Up) { + if (recentAppsHasSelection()) { + _recentApps->selectJump(direction, 0); + } else if (_popularApps->selectJump(direction, 0) + == JumpResult::AppliedAndOut) { + _recentApps->selectJump(direction, -1); + } else if (!popularAppsHasSelection()) { + if (_recentApps->selectJump(direction, 0) + == JumpResult::AppliedAndOut) { + _channelsScroll->scrollTo(0); + } + } + } else if (direction == Qt::Key_Down) { + if (popularAppsHasSelection()) { + _popularApps->selectJump(direction, 0); + } else if (_recentApps->selectJump(direction, 0) + == JumpResult::AppliedAndOut) { + _popularApps->selectJump(direction, 0); + } else if (!recentAppsHasSelection()) { + if (_popularApps->selectJump(direction, 0) + == JumpResult::AppliedAndOut) { + _recentApps->selectJump(direction, 0); + } + } + } +} + void Suggestions::chooseRow() { - if (!_topPeers->chooseRow()) { - _recent->choose(); + switch (_tab.current()) { + case Tab::Chats: + if (!_topPeers->chooseRow()) { + _recent->choose(); + } + break; + case Tab::Channels: + if (!_myChannels->choose()) { + _recommendations->choose(); + } + break; + case Tab::Apps: + if (!_recentApps->choose()) { + _popularApps->choose(); + } + break; } } @@ -1286,6 +1525,13 @@ Data::Thread *Suggestions::updateFromChannelsDrag(QPoint globalPosition) { return fromListId(_recommendations->updateFromParentDrag(globalPosition)); } +Data::Thread *Suggestions::updateFromAppsDrag(QPoint globalPosition) { + if (const auto id = _recentApps->updateFromParentDrag(globalPosition)) { + return fromListId(id); + } + return fromListId(_popularApps->updateFromParentDrag(globalPosition)); +} + Data::Thread *Suggestions::fromListId(uint64 peerListRowId) { return peerListRowId ? _controller->session().data().history(PeerId(peerListRowId)).get() @@ -1297,6 +1543,8 @@ void Suggestions::dragLeft() { _recent->dragLeft(); _myChannels->dragLeft(); _recommendations->dragLeft(); + _recentApps->dragLeft(); + _popularApps->dragLeft(); } void Suggestions::show(anim::type animated, Fn finish) { @@ -1322,7 +1570,8 @@ void Suggestions::hide(anim::type animated, Fn finish) { } void Suggestions::switchTab(Tab tab) { - if (_tab.current() == tab) { + const auto was = _tab.current(); + if (was == tab) { return; } _tab = tab; @@ -1330,19 +1579,29 @@ void Suggestions::switchTab(Tab tab) { if (_tabs->isHidden()) { return; } - startSlideAnimation(); + startSlideAnimation(was, tab); } -void Suggestions::startSlideAnimation() { +void Suggestions::startSlideAnimation(Tab was, Tab now) { if (!_slideAnimation.animating()) { - _slideLeft = Ui::GrabWidget(_chatsScroll.get()); - _slideRight = Ui::GrabWidget(_channelsScroll.get()); + _slideLeft = (was == Tab::Chats || now == Tab::Chats) + ? Ui::GrabWidget(_chatsScroll.get()) + : Ui::GrabWidget(_channelsScroll.get()); + _slideLeftTop = (was == Tab::Chats || now == Tab::Chats) + ? _chatsScroll->y() + : _channelsScroll->y(); + _slideRight = (was == Tab::Apps || now == Tab::Apps) + ? Ui::GrabWidget(_appsScroll.get()) + : Ui::GrabWidget(_channelsScroll.get()); + _slideRightTop = (was == Tab::Apps || now == Tab::Apps) + ? _appsScroll->y() + : _channelsScroll->y(); _chatsScroll->hide(); _channelsScroll->hide(); + _appsScroll->hide(); } - const auto channels = (_tab.current() == Tab::Channels); - const auto from = channels ? 0. : 1.; - const auto to = channels ? 1. : 0.; + const auto from = (now > was) ? 0. : 1.; + const auto to = (now > was) ? 1. : 0.; _slideAnimation.start([=] { update(); if (!_slideAnimation.animating() && !_shownAnimation.animating()) { @@ -1376,20 +1635,23 @@ void Suggestions::startShownAnimation(bool shown, Fn finish) { _tabs->hide(); _chatsScroll->hide(); _channelsScroll->hide(); + _appsScroll->hide(); _slideAnimation.stop(); } void Suggestions::finishShow() { _slideAnimation.stop(); _slideLeft = _slideRight = QPixmap(); + _slideLeftTop = _slideRightTop = 0; _shownAnimation.stop(); _cache = QPixmap(); _tabs->show(); - const auto channels = (_tab.current() == Tab::Channels); - _chatsScroll->setVisible(!channels); - _channelsScroll->setVisible(channels); + const auto tab = _tab.current(); + _chatsScroll->setVisible(tab == Tab::Chats); + _channelsScroll->setVisible(tab == Tab::Channels); + _appsScroll->setVisible(tab == Tab::Apps); } float64 Suggestions::shownOpacity() const { @@ -1414,12 +1676,12 @@ void Suggestions::paintEvent(QPaintEvent *e) { p.setOpacity(1. - progress); p.drawPixmap( anim::interpolate(0, -slide, progress), - _chatsScroll->y(), + _slideLeftTop, _slideLeft); p.setOpacity(progress); p.drawPixmap( anim::interpolate(slide, 0, progress), - _channelsScroll->y(), + _slideRightTop, _slideRight); } } @@ -1434,6 +1696,9 @@ void Suggestions::resizeEvent(QResizeEvent *e) { _channelsScroll->setGeometry(0, tabs, w, height() - tabs); _channelsContent->resizeToWidth(w); + + _appsScroll->setGeometry(0, tabs, w, height() - tabs); + _appsContent->resizeToWidth(w); } auto Suggestions::setupRecentPeers(RecentPeersList recentPeers) @@ -1595,6 +1860,115 @@ auto Suggestions::setupRecommendations() -> std::unique_ptr { return result; } +auto Suggestions::setupRecentApps() -> std::unique_ptr { + const auto controller = lifetime().make_state( + _controller); + + auto result = setupObjectList( + _appsScroll.get(), + _appsContent, + controller); + const auto raw = result.get(); + const auto list = raw->wrap->entity(); + + raw->selectJump = [=](Qt::Key direction, int pageSize) { + const auto had = list->hasSelection(); + if (direction == Qt::Key()) { + return had ? JumpResult::Applied : JumpResult::NotApplied; + } else if (direction == Qt::Key_Up && !had) { + if (pageSize < 0) { + list->selectLast(); + return list->hasSelection() + ? JumpResult::Applied + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { + const auto was = list->selectedIndex(); + const auto delta = (direction == Qt::Key_Down) ? 1 : -1; + if (pageSize > 0) { + list->selectSkipPage(pageSize, delta); + } else { + list->selectSkip(delta); + } + if (had + && delta > 0 + && raw->count.current() + && list->selectedIndex() == was) { + list->clearSelection(); + return JumpResult::AppliedAndOut; + } + return list->hasSelection() + ? JumpResult::Applied + : had + ? JumpResult::AppliedAndOut + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + }; + + raw->chosen.events( + ) | rpl::start_with_next([=] { + _persist = false; + }, list->lifetime()); + + controller->load(); + + return result; +} + +auto Suggestions::setupPopularApps() -> std::unique_ptr { + const auto controller = lifetime().make_state( + _controller); + + const auto addToScroll = [=] { + const auto wrap = _recentApps->wrap; + return wrap->toggled() ? wrap->height() : 0; + }; + auto result = setupObjectList( + _appsScroll.get(), + _appsContent, + controller, + addToScroll); + const auto raw = result.get(); + const auto list = raw->wrap->entity(); + + raw->selectJump = [list](Qt::Key direction, int pageSize) { + const auto had = list->hasSelection(); + if (direction == Qt::Key()) { + return had ? JumpResult::Applied : JumpResult::NotApplied; + } else if (direction == Qt::Key_Up && !had) { + return JumpResult::NotApplied; + } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) { + const auto delta = (direction == Qt::Key_Down) ? 1 : -1; + if (pageSize > 0) { + list->selectSkipPage(pageSize, delta); + } else { + list->selectSkip(delta); + } + return list->hasSelection() + ? JumpResult::Applied + : had + ? JumpResult::AppliedAndOut + : JumpResult::NotApplied; + } + return JumpResult::NotApplied; + }; + + raw->chosen.events( + ) | rpl::start_with_next([=] { + _persist = true; + }, list->lifetime()); + + _tab.value() | rpl::filter( + rpl::mappers::_1 == Tab::Apps + ) | rpl::start_with_next([=] { + controller->load(); + }, list->lifetime()); + + return result; +} + auto Suggestions::setupObjectList( not_null scroll, not_null parent, diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h index 438c0b370..549851d72 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h @@ -79,6 +79,14 @@ public: -> rpl::producer> { return _recommendations->chosen.events(); } + [[nodiscard]] auto recentAppChosen() const + -> rpl::producer> { + return _recentApps->chosen.events(); + } + [[nodiscard]] auto popularAppChosen() const + -> rpl::producer> { + return _popularApps->chosen.events(); + } class ObjectListController; @@ -86,6 +94,7 @@ private: enum class Tab : uchar { Chats, Channels, + Apps, }; enum class JumpResult : uchar { NotApplied, @@ -110,13 +119,16 @@ private: void setupTabs(); void setupChats(); void setupChannels(); + void setupApps(); void selectJumpChats(Qt::Key direction, int pageSize); void selectJumpChannels(Qt::Key direction, int pageSize); + void selectJumpApps(Qt::Key direction, int pageSize); [[nodiscard]] Data::Thread *updateFromChatsDrag(QPoint globalPosition); [[nodiscard]] Data::Thread *updateFromChannelsDrag( QPoint globalPosition); + [[nodiscard]] Data::Thread *updateFromAppsDrag(QPoint globalPosition); [[nodiscard]] Data::Thread *fromListId(uint64 peerListRowId); [[nodiscard]] std::unique_ptr setupRecentPeers( @@ -129,6 +141,9 @@ private: [[nodiscard]] auto setupEmptyChannels() -> object_ptr>; + [[nodiscard]] std::unique_ptr setupRecentApps(); + [[nodiscard]] std::unique_ptr setupPopularApps(); + [[nodiscard]] std::unique_ptr setupObjectList( not_null scroll, not_null parent, @@ -142,7 +157,7 @@ private: void switchTab(Tab tab); void startShownAnimation(bool shown, Fn finish); - void startSlideAnimation(); + void startSlideAnimation(Tab was, Tab now); void finishShow(); void handlePressForChatPreview(PeerId id, Fn callback); @@ -171,6 +186,12 @@ private: const not_null*> _emptyChannels; + const std::unique_ptr _appsScroll; + const not_null _appsContent; + + const std::unique_ptr _recentApps; + const std::unique_ptr _popularApps; + Ui::Animations::Simple _shownAnimation; Fn _showFinished; bool _hidden = false; @@ -181,6 +202,9 @@ private: QPixmap _slideLeft; QPixmap _slideRight; + int _slideLeftTop = 0; + int _slideRightTop = 0; + }; [[nodiscard]] rpl::producer TopPeersContent( diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 8e44a7a79..4d5a9bc04 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -469,6 +469,12 @@ infoSharedMediaButtonIconPosition: point(20px, 3px); infoGroupMembersIconPosition: point(20px, 10px); infoChannelMembersIconPosition: point(20px, 19px); +infoOpenApp: RoundButton(defaultActiveButton) { + textTop: 11px; + height: 40px; +} +infoOpenAppMargin: margins(16px, 12px, 16px, 12px); + infoPersonalChannelIconPosition: point(25px, 20px); infoPersonalChannelNameLabel: FlatLabel(infoProfileStatus) { textFg: windowBoldFg; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 6e17ed549..3c0af7c0d 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_text.h" #include "info/profile/info_profile_values.h" #include "info/profile/info_profile_widget.h" +#include "inline_bots/bot_attach_web_view.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "menu/menu_mute.h" @@ -776,6 +777,7 @@ private: object_ptr setupPersonalChannel(not_null user); object_ptr setupInfo(); object_ptr setupMuteToggle(); + void setupMainApp(); void setupMainButtons(); Ui::MultiSlideTracker fillTopicButtons(); Ui::MultiSlideTracker fillUserButtons( @@ -1209,10 +1211,21 @@ object_ptr DetailsFiller::setupInfo() { } if (!_peer->isSelf()) { // No notifications toggle for Self => no separator. + + const auto user = _peer->asUser(); + const auto app = user && user->botInfo && user->botInfo->hasMainApp; + const auto padding = app + ? QMargins( + st::infoOpenAppMargin.left(), + st::infoProfileSeparatorPadding.top(), + st::infoOpenAppMargin.right(), + 0) + : st::infoProfileSeparatorPadding; + result->add(object_ptr>( result, object_ptr(result), - st::infoProfileSeparatorPadding) + padding) )->setDuration( st::infoSlideDuration )->toggleOn( @@ -1548,6 +1561,42 @@ object_ptr DetailsFiller::setupMuteToggle() { return result; } +void DetailsFiller::setupMainApp() { + const auto button = _wrap->add( + object_ptr( + _wrap, + tr::lng_profile_open_app(), + st::infoOpenApp), + st::infoOpenAppMargin); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + + const auto user = _peer->asUser(); + const auto controller = _controller->parentController(); + button->setClickedCallback([=] { + user->session().attachWebView().open({ + .bot = user, + .context = { + .controller = controller, + .maySkipConfirmation = true, + }, + .source = InlineBots::WebViewSourceBotProfile(), + }); + }); + + const auto url = tr::lng_mini_apps_tos_url(tr::now); + Ui::AddDividerText( + _wrap, + tr::lng_profile_open_app_about( + lt_terms, + tr::lng_profile_open_app_terms() | Ui::Text::ToLink(url), + Ui::Text::WithEntities) + )->setClickHandlerFilter([=](const auto &...) { + UrlClickHandler::Open(url); + return false; + }); + Ui::AddSkip(_wrap); +} + void DetailsFiller::setupMainButtons() { auto wrapButtons = [=](auto &&callback) { auto topSkip = _wrap->add(CreateSlideSkipWidget(_wrap)); @@ -1737,6 +1786,13 @@ object_ptr DetailsFiller::fill() { add(object_ptr(_wrap)); add(CreateSkipWidget(_wrap)); add(setupInfo()); + if (const auto user = _peer->asUser()) { + if (const auto info = user->botInfo.get()) { + if (info->hasMainApp) { + setupMainApp(); + } + } + } if (!_peer->isSelf()) { add(setupMuteToggle()); } diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index e0a609f7a..5810310fa 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -69,6 +69,7 @@ namespace { constexpr auto kProlongTimeout = 60 * crl::time(1000); constexpr auto kRefreshBotsTimeout = 60 * 60 * crl::time(1000); +constexpr auto kPopularAppBotsLimit = 100; [[nodiscard]] DocumentData *ResolveIcon( not_null session, @@ -669,9 +670,13 @@ void WebViewInstance::resolve() { }, [&](WebViewSourceGame game) { showGame(); }, [&](WebViewSourceBotProfile) { - confirmOpen([=] { + if (_context.maySkipConfirmation) { requestMain(); - }); + } else { + confirmOpen([=] { + requestMain(); + }); + } }); } @@ -757,6 +762,10 @@ void WebViewInstance::confirmOpen(Fn done) { close(); done(); }; + const auto cancel = [=](Fn close) { + botClose(); + close(); + }; _parentShow->show(Ui::MakeConfirmBox({ .text = tr::lng_allow_bot_webview( tr::now, @@ -764,7 +773,7 @@ void WebViewInstance::confirmOpen(Fn done) { Ui::Text::Bold(_bot->name()), Ui::Text::RichLangValue), .confirmed = crl::guard(this, callback), - .cancelled = crl::guard(this, [=] { botClose(); }), + .cancelled = crl::guard(this, cancel), .confirmText = tr::lng_box_ok(), })); } @@ -1444,7 +1453,10 @@ AttachWebView::AttachWebView(not_null session) _refreshTimer.callEach(kRefreshBotsTimeout); } -AttachWebView::~AttachWebView() = default; +AttachWebView::~AttachWebView() { + closeAll(); + _session->api().request(_popularAppBotsRequestId).cancel(); +} void AttachWebView::openByUsername( not_null controller, @@ -1501,6 +1513,40 @@ void AttachWebView::closeAll() { base::take(_instances); } +void AttachWebView::loadPopularAppBots() { + if (_popularAppBotsLoaded.current() || _popularAppBotsRequestId) { + return; + } + _popularAppBotsRequestId = _session->api().request( + MTPbots_GetPopularAppBots( + MTP_string(), + MTP_int(kPopularAppBotsLimit)) + ).done([=](const MTPbots_PopularAppBots &result) { + _popularAppBotsRequestId = 0; + + const auto &list = result.data().vusers().v; + auto parsed = std::vector>(); + parsed.reserve(list.size()); + for (const auto &user : list) { + const auto bot = _session->data().processUser(user); + if (bot->isBot()) { + parsed.push_back(bot); + } + } + _popularAppBots = std::move(parsed); + _popularAppBotsLoaded = true; + }).send(); +} + +auto AttachWebView::popularAppBots() const +-> const std::vector> & { + return _popularAppBots; +} + +rpl::producer<> AttachWebView::popularAppBotsLoaded() const { + return _popularAppBotsLoaded.changes() | rpl::to_empty; +} + void AttachWebView::cancel() { _session->api().request(base::take(_requestId)).cancel(); _botUsername = QString(); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index ecccdd042..4a4f5ca08 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -256,9 +256,6 @@ private: void showGame(); void started(uint64 queryId); - [[nodiscard]] Window::SessionController *windowForThread( - not_null thread); - auto nonPanelPaymentFormFactory( Fn reactivate) -> Fn; @@ -352,6 +349,11 @@ public: void close(not_null instance); void closeAll(); + void loadPopularAppBots(); + [[nodiscard]] auto popularAppBots() const + -> const std::vector> &; + [[nodiscard]] rpl::producer<> popularAppBotsLoaded() const; + private: void resolveUsername( std::shared_ptr show, @@ -395,6 +397,10 @@ private: std::vector> _instances; + std::vector> _popularAppBots; + mtpRequestId _popularAppBotsRequestId = 0; + rpl::variable _popularAppBotsLoaded = false; + }; [[nodiscard]] std::unique_ptr MakeAttachBotsMenu( diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index cc09dacf5..5dd80e988 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -110,7 +110,9 @@ Session::Session( , _recentPeers(std::make_unique(this)) , _scheduledMessages(std::make_unique(this)) , _sponsoredMessages(std::make_unique(this)) -, _topPeers(std::make_unique(this)) +, _topPeers(std::make_unique(this, Data::TopPeerType::Chat)) +, _topBotApps( + std::make_unique(this, Data::TopPeerType::BotApp)) , _factchecks(std::make_unique(this)) , _locationPickers(std::make_unique()) , _cachedReactionIconFactory(std::make_unique()) diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index a69373a36..ba5dbcd99 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -129,6 +129,9 @@ public: [[nodiscard]] Data::TopPeers &topPeers() const { return *_topPeers; } + [[nodiscard]] Data::TopPeers &topBotApps() const { + return *_topBotApps; + } [[nodiscard]] Data::Factchecks &factchecks() const { return *_factchecks; } @@ -262,6 +265,7 @@ private: const std::unique_ptr _scheduledMessages; const std::unique_ptr _sponsoredMessages; const std::unique_ptr _topPeers; + const std::unique_ptr _topBotApps; const std::unique_ptr _factchecks; const std::unique_ptr _locationPickers; diff --git a/Telegram/SourceFiles/ui/vertical_list.cpp b/Telegram/SourceFiles/ui/vertical_list.cpp index 11347aa61..6666077dd 100644 --- a/Telegram/SourceFiles/ui/vertical_list.cpp +++ b/Telegram/SourceFiles/ui/vertical_list.cpp @@ -28,31 +28,34 @@ void AddDivider(not_null container) { container->add(object_ptr(container)); } -void AddDividerText( +not_null AddDividerText( not_null container, rpl::producer text, const style::margins &margins, RectParts parts) { - AddDividerText( + return AddDividerText( container, std::move(text) | Ui::Text::ToWithEntities(), margins, parts); } -void AddDividerText( +not_null AddDividerText( not_null container, rpl::producer text, const style::margins &margins, RectParts parts) { + auto label = object_ptr( + container, + std::move(text), + st::boxDividerLabel); + const auto result = label.data(); container->add(object_ptr( container, - object_ptr( - container, - std::move(text), - st::boxDividerLabel), + std::move(label), margins, parts)); + return result; } not_null AddSubsectionTitle( diff --git a/Telegram/SourceFiles/ui/vertical_list.h b/Telegram/SourceFiles/ui/vertical_list.h index 7ab743bd3..82934367f 100644 --- a/Telegram/SourceFiles/ui/vertical_list.h +++ b/Telegram/SourceFiles/ui/vertical_list.h @@ -25,12 +25,12 @@ class VerticalLayout; void AddSkip(not_null container); void AddSkip(not_null container, int skip); void AddDivider(not_null container); -void AddDividerText( +not_null AddDividerText( not_null container, rpl::producer text, const style::margins &margins = st::defaultBoxDividerLabelPadding, RectParts parts = RectPart::Top | RectPart::Bottom); -void AddDividerText( +not_null AddDividerText( not_null container, rpl::producer text, const style::margins &margins = st::defaultBoxDividerLabelPadding,