mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-16 06:07:06 +02:00
Implement recent/popular apps list.
This commit is contained in:
parent
031233ea98
commit
6a8a85e395
17 changed files with 792 additions and 189 deletions
|
@ -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";
|
||||
|
|
|
@ -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 &) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>())
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue