Implement recent/popular apps list.

This commit is contained in:
John Preston 2024-07-25 18:08:22 +02:00
parent 031233ea98
commit 6a8a85e395
17 changed files with 792 additions and 189 deletions

View file

@ -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";

View file

@ -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<Main::Session*> session)
: _session(session) {
TopPeers::TopPeers(not_null<Main::Session*> 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<PeerData*> 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 &) {

View file

@ -13,9 +13,14 @@ class Session;
namespace Data {
enum class TopPeerType {
Chat,
BotApp,
};
class TopPeers final {
public:
explicit TopPeers(not_null<Main::Session*> session);
TopPeers(not_null<Main::Session*> session, TopPeerType type);
~TopPeers();
[[nodiscard]] std::vector<not_null<PeerData*>> list() const;
@ -36,11 +41,13 @@ private:
float64 rating = 0.;
};
void loadAfterChats();
void request();
[[nodiscard]] uint64 countHash() const;
void updated();
const not_null<Main::Session*> _session;
const TopPeerType _type = {};
std::vector<TopPeer> _list;
rpl::event_stream<> _updates;

View file

@ -721,6 +721,8 @@ not_null<UserData*> 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);
}

View file

@ -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 {

View file

@ -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<PeerData*> 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<PeerData*> 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<UserData*> bot) {
session().attachWebView().open({
.bot = bot,
.context = {
.controller = controller(),
.maySkipConfirmation = true,
},
.source = InlineBots::WebViewSourceBotProfile(),
});
}
void Widget::changeOpenedSubsection(
FnMut<void()> change,
bool fromRight,

View file

@ -214,6 +214,7 @@ private:
void refreshTopBars();
void showSearchInTopBar(anim::type animated);
void checkUpdateStatus();
void openBotMainApp(not_null<UserData*> bot);
void changeOpenedSubsection(
FnMut<void()> change,
bool fromRight,

View file

@ -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<PeerListRow*> row) override;
bool rowTrackPress(not_null<PeerListRow*> 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<PeerData*> peer);
[[nodiscard]] bool expandedCurrent() const;
[[nodiscard]] rpl::producer<bool> expanded() const;
void setupPlainDivider(rpl::producer<QString> title);
void setupExpandDivider(rpl::producer<QString> title);
private:
const not_null<Window::SessionController*> _window;
@ -334,6 +347,8 @@ private:
rpl::event_stream<> _touchCancelRequests;
rpl::event_stream<not_null<PeerData*>> _chosen;
rpl::variable<int> _count;
rpl::variable<Ui::RpWidget*> _toggleExpanded = nullptr;
rpl::variable<bool> _expanded = false;
};
@ -344,11 +359,9 @@ public:
RecentPeersList list);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
Main::Session &session() const override;
QString savedMessagesChatStatus() const override;
@ -369,20 +382,15 @@ public:
not_null<Window::SessionController*> window);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
Main::Session &session() const override;
private:
void setupDivider();
void appendRow(not_null<ChannelData*> channel);
void fill(bool force = false);
std::vector<not_null<History*>> _channels;
rpl::variable<Ui::RpWidget*> _toggleExpanded = nullptr;
rpl::variable<bool> _expanded = false;
rpl::lifetime _lifetime;
};
@ -394,17 +402,11 @@ public:
not_null<Window::SessionController*> window);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
Main::Session &session() const override;
void load();
private:
void fill();
void setupDivider();
void appendRow(not_null<ChannelData*> channel);
History *_activeHistory = nullptr;
@ -413,6 +415,45 @@ private:
};
class RecentAppsController final
: public Suggestions::ObjectListController {
public:
explicit RecentAppsController(
not_null<Window::SessionController*> window);
void prepare() override;
void load();
private:
void appendRow(not_null<UserData*> bot);
void fill();
std::vector<not_null<UserData*>> _bots;
rpl::lifetime _lifetime;
};
class PopularAppsController final
: public Suggestions::ObjectListController {
public:
explicit PopularAppsController(
not_null<Window::SessionController*> window);
void prepare() override;
void load();
private:
void fill();
void appendRow(not_null<UserData*> bot);
History *_activeHistory = nullptr;
bool _requested = false;
rpl::lifetime _lifetime;
};
Suggestions::ObjectListController::ObjectListController(
not_null<Window::SessionController*> window)
: _window(window) {
@ -510,8 +551,108 @@ void Suggestions::ObjectListController::setCount(int count) {
_count = count;
}
void Suggestions::ObjectListController::choose(not_null<PeerData*> peer) {
_chosen.fire_copy(peer);
bool Suggestions::ObjectListController::expandedCurrent() const {
return _expanded.current();
}
rpl::producer<bool> Suggestions::ObjectListController::expanded() const {
return _expanded.value();
}
void Suggestions::ObjectListController::rowClicked(
not_null<PeerListRow*> row) {
_chosen.fire(row->peer());
}
void Suggestions::ObjectListController::setupPlainDivider(
rpl::producer<QString> title) {
auto result = object_ptr<Ui::FixedHeightWidget>(
(QWidget*)nullptr,
st::searchedBarHeight);
const auto raw = result.data();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
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<QString> title) {
auto result = object_ptr<Ui::FixedHeightWidget>(
(QWidget*)nullptr,
st::searchedBarHeight);
const auto raw = result.data();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
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<Ui::LinkButton>(
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<PeerListRow*> row) {
choose(row->peer());
}
Fn<void()> RecentsController::removeAllCallback() {
const auto weak = base::make_weak(this);
const auto session = &this->session();
@ -584,10 +721,6 @@ base::unique_qptr<Ui::PopupMenu> 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<ChannelData*> channel) {
delegate()->peerListAppendRow(std::move(row));
}
void MyChannelsController::rowClicked(not_null<PeerListRow*> row) {
choose(row->peer());
}
base::unique_qptr<Ui::PopupMenu> MyChannelsController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
@ -798,83 +927,13 @@ base::unique_qptr<Ui::PopupMenu> MyChannelsController::rowContextMenu(
return result;
}
Main::Session &MyChannelsController::session() const {
return window()->session();
}
void MyChannelsController::setupDivider() {
auto result = object_ptr<Ui::FixedHeightWidget>(
(QWidget*)nullptr,
st::searchedBarHeight);
const auto raw = result.data();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
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<Ui::LinkButton>(
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::SessionController*> window)
: ObjectListController(window) {
}
void RecommendationsController::prepare() {
setupDivider();
setupPlainDivider(tr::lng_channels_recommended());
fill();
}
@ -940,41 +999,115 @@ void RecommendationsController::appendRow(not_null<ChannelData*> channel) {
delegate()->peerListAppendRow(std::move(row));
}
void RecommendationsController::rowClicked(not_null<PeerListRow*> row) {
choose(row->peer());
RecentAppsController::RecentAppsController(
not_null<Window::SessionController*> window)
: ObjectListController(window) {
}
base::unique_qptr<Ui::PopupMenu> RecommendationsController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> 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<Ui::FixedHeightWidget>(
(QWidget*)nullptr,
st::searchedBarHeight);
const auto raw = result.data();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
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<UserData*> bot) {
auto row = std::make_unique<PeerListRow>(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::SessionController*> 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<UserData*> bot) {
auto row = std::make_unique<PeerListRow>(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<Ui::VerticalLayout>(this)))
, _myChannels(setupMyChannels())
, _recommendations(setupRecommendations())
, _emptyChannels(_channelsContent->add(setupEmptyChannels())) {
, _emptyChannels(_channelsContent->add(setupEmptyChannels()))
, _appsScroll(std::make_unique<Ui::ElasticScroll>(this))
, _appsContent(
_appsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(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<QTouchEvent*> 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<void()> finish) {
@ -1322,7 +1570,8 @@ void Suggestions::hide(anim::type animated, Fn<void()> 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<void()> 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<ObjectList> {
return result;
}
auto Suggestions::setupRecentApps() -> std::unique_ptr<ObjectList> {
const auto controller = lifetime().make_state<RecentAppsController>(
_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<ObjectList> {
const auto controller = lifetime().make_state<PopularAppsController>(
_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<Ui::ElasticScroll*> scroll,
not_null<Ui::VerticalLayout*> parent,

View file

@ -79,6 +79,14 @@ public:
-> rpl::producer<not_null<PeerData*>> {
return _recommendations->chosen.events();
}
[[nodiscard]] auto recentAppChosen() const
-> rpl::producer<not_null<PeerData*>> {
return _recentApps->chosen.events();
}
[[nodiscard]] auto popularAppChosen() const
-> rpl::producer<not_null<PeerData*>> {
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<ObjectList> setupRecentPeers(
@ -129,6 +141,9 @@ private:
[[nodiscard]] auto setupEmptyChannels()
-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;
[[nodiscard]] std::unique_ptr<ObjectList> setupRecentApps();
[[nodiscard]] std::unique_ptr<ObjectList> setupPopularApps();
[[nodiscard]] std::unique_ptr<ObjectList> setupObjectList(
not_null<Ui::ElasticScroll*> scroll,
not_null<Ui::VerticalLayout*> parent,
@ -142,7 +157,7 @@ private:
void switchTab(Tab tab);
void startShownAnimation(bool shown, Fn<void()> finish);
void startSlideAnimation();
void startSlideAnimation(Tab was, Tab now);
void finishShow();
void handlePressForChatPreview(PeerId id, Fn<void(bool)> callback);
@ -171,6 +186,12 @@ private:
const not_null<Ui::SlideWrap<Ui::RpWidget>*> _emptyChannels;
const std::unique_ptr<Ui::ElasticScroll> _appsScroll;
const not_null<Ui::VerticalLayout*> _appsContent;
const std::unique_ptr<ObjectList> _recentApps;
const std::unique_ptr<ObjectList> _popularApps;
Ui::Animations::Simple _shownAnimation;
Fn<void()> _showFinished;
bool _hidden = false;
@ -181,6 +202,9 @@ private:
QPixmap _slideLeft;
QPixmap _slideRight;
int _slideLeftTop = 0;
int _slideRightTop = 0;
};
[[nodiscard]] rpl::producer<TopPeersList> TopPeersContent(

View file

@ -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;

View file

@ -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<Ui::RpWidget> setupPersonalChannel(not_null<UserData*> user);
object_ptr<Ui::RpWidget> setupInfo();
object_ptr<Ui::RpWidget> setupMuteToggle();
void setupMainApp();
void setupMainButtons();
Ui::MultiSlideTracker fillTopicButtons();
Ui::MultiSlideTracker fillUserButtons(
@ -1209,10 +1211,21 @@ object_ptr<Ui::RpWidget> 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<Ui::SlideWrap<>>(
result,
object_ptr<Ui::PlainShadow>(result),
st::infoProfileSeparatorPadding)
padding)
)->setDuration(
st::infoSlideDuration
)->toggleOn(
@ -1548,6 +1561,42 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupMuteToggle() {
return result;
}
void DetailsFiller::setupMainApp() {
const auto button = _wrap->add(
object_ptr<Ui::RoundButton>(
_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<Ui::RpWidget> DetailsFiller::fill() {
add(object_ptr<Ui::BoxContentDivider>(_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());
}

View file

@ -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<Main::Session*> 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<void()> done) {
close();
done();
};
const auto cancel = [=](Fn<void()> close) {
botClose();
close();
};
_parentShow->show(Ui::MakeConfirmBox({
.text = tr::lng_allow_bot_webview(
tr::now,
@ -764,7 +773,7 @@ void WebViewInstance::confirmOpen(Fn<void()> 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<Main::Session*> session)
_refreshTimer.callEach(kRefreshBotsTimeout);
}
AttachWebView::~AttachWebView() = default;
AttachWebView::~AttachWebView() {
closeAll();
_session->api().request(_popularAppBotsRequestId).cancel();
}
void AttachWebView::openByUsername(
not_null<Window::SessionController*> 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<not_null<UserData*>>();
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<not_null<UserData*>> & {
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();

View file

@ -256,9 +256,6 @@ private:
void showGame();
void started(uint64 queryId);
[[nodiscard]] Window::SessionController *windowForThread(
not_null<Data::Thread*> thread);
auto nonPanelPaymentFormFactory(
Fn<void(Payments::CheckoutResult)> reactivate)
-> Fn<void(Payments::NonPanelPaymentForm)>;
@ -352,6 +349,11 @@ public:
void close(not_null<WebViewInstance*> instance);
void closeAll();
void loadPopularAppBots();
[[nodiscard]] auto popularAppBots() const
-> const std::vector<not_null<UserData*>> &;
[[nodiscard]] rpl::producer<> popularAppBotsLoaded() const;
private:
void resolveUsername(
std::shared_ptr<Ui::Show> show,
@ -395,6 +397,10 @@ private:
std::vector<std::unique_ptr<WebViewInstance>> _instances;
std::vector<not_null<UserData*>> _popularAppBots;
mtpRequestId _popularAppBotsRequestId = 0;
rpl::variable<bool> _popularAppBotsLoaded = false;
};
[[nodiscard]] std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(

View file

@ -110,7 +110,9 @@ Session::Session(
, _recentPeers(std::make_unique<Data::RecentPeers>(this))
, _scheduledMessages(std::make_unique<Data::ScheduledMessages>(this))
, _sponsoredMessages(std::make_unique<Data::SponsoredMessages>(this))
, _topPeers(std::make_unique<Data::TopPeers>(this))
, _topPeers(std::make_unique<Data::TopPeers>(this, Data::TopPeerType::Chat))
, _topBotApps(
std::make_unique<Data::TopPeers>(this, Data::TopPeerType::BotApp))
, _factchecks(std::make_unique<Data::Factchecks>(this))
, _locationPickers(std::make_unique<Data::LocationPickers>())
, _cachedReactionIconFactory(std::make_unique<ReactionIconFactory>())

View file

@ -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<Data::ScheduledMessages> _scheduledMessages;
const std::unique_ptr<Data::SponsoredMessages> _sponsoredMessages;
const std::unique_ptr<Data::TopPeers> _topPeers;
const std::unique_ptr<Data::TopPeers> _topBotApps;
const std::unique_ptr<Data::Factchecks> _factchecks;
const std::unique_ptr<Data::LocationPickers> _locationPickers;

View file

@ -28,31 +28,34 @@ void AddDivider(not_null<Ui::VerticalLayout*> container) {
container->add(object_ptr<Ui::BoxContentDivider>(container));
}
void AddDividerText(
not_null<Ui::FlatLabel*> AddDividerText(
not_null<Ui::VerticalLayout*> container,
rpl::producer<QString> text,
const style::margins &margins,
RectParts parts) {
AddDividerText(
return AddDividerText(
container,
std::move(text) | Ui::Text::ToWithEntities(),
margins,
parts);
}
void AddDividerText(
not_null<Ui::FlatLabel*> AddDividerText(
not_null<Ui::VerticalLayout*> container,
rpl::producer<TextWithEntities> text,
const style::margins &margins,
RectParts parts) {
auto label = object_ptr<Ui::FlatLabel>(
container,
std::move(text),
st::boxDividerLabel);
const auto result = label.data();
container->add(object_ptr<Ui::DividerLabel>(
container,
object_ptr<Ui::FlatLabel>(
container,
std::move(text),
st::boxDividerLabel),
std::move(label),
margins,
parts));
return result;
}
not_null<Ui::FlatLabel*> AddSubsectionTitle(

View file

@ -25,12 +25,12 @@ class VerticalLayout;
void AddSkip(not_null<Ui::VerticalLayout*> container);
void AddSkip(not_null<Ui::VerticalLayout*> container, int skip);
void AddDivider(not_null<Ui::VerticalLayout*> container);
void AddDividerText(
not_null<Ui::FlatLabel*> AddDividerText(
not_null<Ui::VerticalLayout*> container,
rpl::producer<QString> text,
const style::margins &margins = st::defaultBoxDividerLabelPadding,
RectParts parts = RectPart::Top | RectPart::Bottom);
void AddDividerText(
not_null<Ui::FlatLabel*> AddDividerText(
not_null<Ui::VerticalLayout*> container,
rpl::producer<TextWithEntities> text,
const style::margins &margins = st::defaultBoxDividerLabelPadding,