tdesktop/Telegram/SourceFiles/info/statistics/info_statistics_list_contro...

847 lines
22 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/statistics/info_statistics_list_controllers.h"
#include "api/api_statistics.h"
#include "boxes/peer_list_controllers.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_user.h"
#include "history/history_item.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/effects/outline_segments.h" // Ui::UnreadStoryOutlineGradient.
#include "ui/effects/toggle_arrow.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "styles/style_dialogs.h" // dialogsStoriesFull.
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include "styles/style_statistics.h"
#include "styles/style_window.h"
namespace Info::Statistics {
namespace {
using BoostCallback = Fn<void(const Data::Boost &)>;
constexpr auto kColorIndexUnclaimed = int(3);
constexpr auto kColorIndexPending = int(4);
void AddArrow(not_null<Ui::RpWidget*> parent) {
const auto arrow = Ui::CreateChild<Ui::RpWidget>(parent.get());
arrow->paintRequest(
) | rpl::start_with_next([=](const QRect &r) {
auto p = QPainter(arrow);
const auto path = Ui::ToggleUpDownArrowPath(
st::statisticsShowMoreButtonArrowSize,
st::statisticsShowMoreButtonArrowSize,
st::statisticsShowMoreButtonArrowSize,
st::mainMenuToggleFourStrokes,
0.);
auto hq = PainterHighQualityEnabler(p);
p.fillPath(path, st::lightButtonFg);
}, arrow->lifetime());
arrow->resize(Size(st::statisticsShowMoreButtonArrowSize * 2));
arrow->move(st::statisticsShowMoreButtonArrowPosition);
arrow->show();
}
void AddSubtitle(
not_null<Ui::VerticalLayout*> container,
rpl::producer<QString> title) {
const auto &subtitlePadding = st::settingsButton.padding;
Ui::AddSubsectionTitle(
container,
std::move(title),
{ 0, -subtitlePadding.top(), 0, -subtitlePadding.bottom() });
}
[[nodiscard]] QString FormatText(
int value1, tr::phrase<lngtag_count> phrase1,
int value2, tr::phrase<lngtag_count> phrase2,
int value3, tr::phrase<lngtag_count> phrase3) {
const auto separator = u", "_q;
auto resultText = QString();
if (value1 > 0) {
resultText += phrase1(tr::now, lt_count, value1);
}
if (value2 > 0) {
if (!resultText.isEmpty()) {
resultText += separator;
}
resultText += phrase2(tr::now, lt_count, value2);
}
if (value3 > 0) {
if (!resultText.isEmpty()) {
resultText += separator;
}
resultText += phrase3(tr::now, lt_count, value3);
}
return resultText;
}
struct PublicForwardsDescriptor final {
Data::PublicForwardsSlice firstSlice;
Fn<void(Data::RecentPostId)> requestShow;
not_null<PeerData*> peer;
Data::RecentPostId contextId;
};
struct MembersDescriptor final {
not_null<Main::Session*> session;
Fn<void(not_null<PeerData*>)> showPeerInfo;
Data::SupergroupStatistics data;
};
struct BoostsDescriptor final {
Data::BoostsListSlice firstSlice;
BoostCallback boostClickedCallback;
not_null<PeerData*> peer;
};
class PeerListRowWithFullId : public PeerListRow {
public:
PeerListRowWithFullId(
not_null<PeerData*> peer,
Data::RecentPostId contextId);
[[nodiscard]] PaintRoundImageCallback generatePaintUserpicCallback(
bool) override;
[[nodiscard]] Data::RecentPostId contextId() const;
private:
const Data::RecentPostId _contextId;
};
PeerListRowWithFullId::PeerListRowWithFullId(
not_null<PeerData*> peer,
Data::RecentPostId contextId)
: PeerListRow(peer)
, _contextId(contextId) {
}
PaintRoundImageCallback PeerListRowWithFullId::generatePaintUserpicCallback(
bool forceRound) {
if (!_contextId.storyId) {
return PeerListRow::generatePaintUserpicCallback(forceRound);
}
const auto peer = PeerListRow::peer();
auto userpic = PeerListRow::ensureUserpicView();
const auto line = st::dialogsStoriesFull.lineTwice;
const auto penWidth = line / 2.;
const auto offset = 1.5 * penWidth * 2;
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
const auto rect = QRect(QPoint(x, y), Size(size));
peer->paintUserpicLeft(
p,
userpic,
x + offset,
y + offset,
outerWidth,
size - offset * 2);
auto hq = PainterHighQualityEnabler(p);
auto gradient = Ui::UnreadStoryOutlineGradient();
gradient.setStart(rect.topRight());
gradient.setFinalStop(rect.bottomLeft());
p.setPen(QPen(gradient, penWidth));
p.setBrush(Qt::NoBrush);
p.drawEllipse(rect - Margins(penWidth));
};
}
Data::RecentPostId PeerListRowWithFullId::contextId() const {
return _contextId;
}
class MembersController final : public PeerListController {
public:
MembersController(MembersDescriptor d);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
void setLimit(int limit);
private:
void addRows(int from, int to);
const not_null<Main::Session*> _session;
Fn<void(not_null<PeerData*>)> _showPeerInfo;
Data::SupergroupStatistics _data;
int _limit = 0;
};
MembersController::MembersController(MembersDescriptor d)
: _session(std::move(d.session))
, _showPeerInfo(std::move(d.showPeerInfo))
, _data(std::move(d.data)) {
}
Main::Session &MembersController::session() const {
return *_session;
}
void MembersController::setLimit(int limit) {
addRows(_limit, limit);
_limit = limit;
}
void MembersController::addRows(int from, int to) {
const auto addRow = [&](UserId userId, QString text) {
const auto user = _session->data().user(userId);
auto row = std::make_unique<PeerListRow>(user);
row->setCustomStatus(std::move(text));
delegate()->peerListAppendRow(std::move(row));
};
if (!_data.topSenders.empty()) {
for (auto i = from; i < to; i++) {
const auto &member = _data.topSenders[i];
addRow(
member.userId,
FormatText(
member.sentMessageCount,
tr::lng_stats_member_messages,
member.averageCharacterCount,
tr::lng_stats_member_characters,
0,
{}));
}
} else if (!_data.topAdministrators.empty()) {
for (auto i = from; i < to; i++) {
const auto &admin = _data.topAdministrators[i];
addRow(
admin.userId,
FormatText(
admin.deletedMessageCount,
tr::lng_stats_member_deletions,
admin.bannedUserCount,
tr::lng_stats_member_bans,
admin.restrictedUserCount,
tr::lng_stats_member_restrictions));
}
} else if (!_data.topInviters.empty()) {
for (auto i = from; i < to; i++) {
const auto &inviter = _data.topInviters[i];
addRow(
inviter.userId,
FormatText(
inviter.addedMemberCount,
tr::lng_stats_member_invitations,
0,
{},
0,
{}));
}
}
}
void MembersController::prepare() {
}
void MembersController::loadMoreRows() {
}
void MembersController::rowClicked(not_null<PeerListRow*> row) {
crl::on_main([=, peer = row->peer()] {
_showPeerInfo(peer);
});
}
class PublicForwardsController final : public PeerListController {
public:
explicit PublicForwardsController(PublicForwardsDescriptor d);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
private:
void appendRow(not_null<PeerData*> peer, Data::RecentPostId contextId);
void applySlice(const Data::PublicForwardsSlice &slice);
const not_null<Main::Session*> _session;
Fn<void(Data::RecentPostId)> _requestShow;
Api::PublicForwards _api;
Data::PublicForwardsSlice _firstSlice;
Data::PublicForwardsSlice::OffsetToken _apiToken;
bool _allLoaded = false;
};
PublicForwardsController::PublicForwardsController(PublicForwardsDescriptor d)
: _session(&d.peer->session())
, _requestShow(std::move(d.requestShow))
, _api(d.peer->asChannel(), d.contextId)
, _firstSlice(std::move(d.firstSlice)) {
}
Main::Session &PublicForwardsController::session() const {
return *_session;
}
void PublicForwardsController::prepare() {
applySlice(base::take(_firstSlice));
delegate()->peerListRefreshRows();
}
void PublicForwardsController::loadMoreRows() {
if (_allLoaded) {
return;
}
_api.request(_apiToken, [=](const Data::PublicForwardsSlice &slice) {
applySlice(slice);
});
}
void PublicForwardsController::applySlice(
const Data::PublicForwardsSlice &slice) {
_allLoaded = slice.allLoaded;
_apiToken = slice.token;
for (const auto &item : slice.list) {
if (const auto &full = item.messageId) {
if (const auto peer = session().data().peerLoaded(full.peer)) {
appendRow(peer, item);
}
} else if (const auto &full = item.storyId) {
if (const auto story = session().data().stories().lookup(full)) {
appendRow((*story)->peer(), item);
}
}
}
delegate()->peerListRefreshRows();
}
void PublicForwardsController::rowClicked(not_null<PeerListRow*> row) {
const auto rowWithId = static_cast<PeerListRowWithFullId*>(row.get());
crl::on_main([=, id = rowWithId->contextId()] { _requestShow(id); });
}
base::unique_qptr<Ui::PopupMenu> PublicForwardsController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
auto menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
const auto peer = row->peer();
const auto text = (peer->isChat() || peer->isMegagroup())
? tr::lng_context_view_group(tr::now)
: peer->isUser()
? tr::lng_context_view_profile(tr::now)
: peer->isChannel()
? tr::lng_context_view_channel(tr::now)
: QString();
if (text.isEmpty()) {
return nullptr;
}
menu->addAction(text, crl::guard(parent, [=, peerId = peer->id] {
_requestShow({ .messageId = { peerId, MsgId() } });
}), peer->isUser() ? &st::menuIconProfile : &st::menuIconInfo);
return menu;
}
void PublicForwardsController::appendRow(
not_null<PeerData*> peer,
Data::RecentPostId contextId) {
if (delegate()->peerListFindRow(peer->id.value)) {
return;
}
auto row = std::make_unique<PeerListRowWithFullId>(peer, contextId);
const auto members = peer->isChannel()
? peer->asChannel()->membersCount()
: 0;
const auto views = [&] {
if (contextId.messageId) {
const auto message = peer->owner().message(contextId.messageId);
return message ? message->viewsCount() : 0;
} else if (const auto &id = contextId.storyId) {
const auto story = peer->owner().stories().lookup(id);
return story ? (*story)->views() : 0;
}
return 0;
}();
const auto membersText = !members
? QString()
: peer->isMegagroup()
? tr::lng_chat_status_members(tr::now, lt_count_decimal, members)
: tr::lng_chat_status_subscribers(tr::now, lt_count_decimal, members);
const auto viewsText = views
? tr::lng_stats_recent_messages_views({}, lt_count_decimal, views)
: QString();
const auto resultText = (membersText.isEmpty() && viewsText.isEmpty())
? tr::lng_stories_no_views(tr::now)
: (membersText.isEmpty() || viewsText.isEmpty())
? membersText + viewsText
: QString("%1, %2").arg(membersText, viewsText);
row->setCustomStatus(resultText);
delegate()->peerListAppendRow(std::move(row));
return;
}
class BoostRow final : public PeerListRow {
public:
BoostRow(not_null<PeerData*> peer, const Data::Boost &boost);
BoostRow(const Data::Boost &boost);
[[nodiscard]] const Data::Boost &boost() const;
[[nodiscard]] QString generateName() override;
[[nodiscard]] PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
int paintNameIconGetWidth(
Painter &p,
Fn<void()> repaint,
crl::time now,
int nameLeft,
int nameTop,
int nameWidth,
int availableWidth,
int outerWidth,
bool selected) override;
QSize rightActionSize() const override;
QMargins rightActionMargins() const override;
bool rightActionDisabled() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
private:
void init();
void invalidateBadges();
const Data::Boost _boost;
Ui::EmptyUserpic _userpic;
QImage _badge;
QImage _rightBadge;
};
BoostRow::BoostRow(not_null<PeerData*> peer, const Data::Boost &boost)
: PeerListRow(peer, UniqueRowIdFromString(boost.id))
, _boost(boost)
, _userpic(Ui::EmptyUserpic::UserpicColor(0), QString()) {
init();
}
BoostRow::BoostRow(const Data::Boost &boost)
: PeerListRow(UniqueRowIdFromString(boost.id))
, _boost(boost)
, _userpic(
Ui::EmptyUserpic::UserpicColor(boost.isUnclaimed
? kColorIndexUnclaimed
: kColorIndexPending),
QString()) {
init();
}
void BoostRow::init() {
invalidateBadges();
auto status = !PeerListRow::special()
? tr::lng_boosts_list_status(
tr::now,
lt_date,
langDayOfMonth(_boost.expiresAt.date()))
: tr::lng_months_tiny(tr::now, lt_count, _boost.expiresAfterMonths)
+ ' '
+ QChar(0x2022)
+ ' '
+ langDayOfMonth(_boost.date.date());
PeerListRow::setCustomStatus(std::move(status));
}
const Data::Boost &BoostRow::boost() const {
return _boost;
}
QString BoostRow::generateName() {
return !PeerListRow::special()
? PeerListRow::generateName()
: _boost.isUnclaimed
? tr::lng_boosts_list_unclaimed(tr::now)
: tr::lng_boosts_list_pending(tr::now);
}
PaintRoundImageCallback BoostRow::generatePaintUserpicCallback(bool force) {
if (!PeerListRow::special()) {
return PeerListRow::generatePaintUserpicCallback(force);
}
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
_userpic.paintCircle(p, x, y, outerWidth, size);
(_boost.isUnclaimed
? st::boostsListUnclaimedIcon
: st::boostsListUnknownIcon).paintInCenter(
p,
{ x, y, size, size });
};
}
void BoostRow::invalidateBadges() {
_badge = _boost.multiplier
? CreateBadge(
st::statisticsDetailsBottomCaptionStyle,
QString::number(_boost.multiplier),
st::boostsListBadgeHeight,
st::boostsListBadgeTextPadding,
st::premiumButtonBg2,
st::premiumButtonFg,
1.,
st::boostsListMiniIconPadding,
st::boostsListMiniIcon)
: QImage();
constexpr auto kBadgeBgOpacity = 0.2;
const auto &rightColor = _boost.isGiveaway
? st::historyPeer4UserpicBg2
: st::historyPeer8UserpicBg2;
const auto &rightIcon = _boost.isGiveaway
? st::boostsListGiveawayMiniIcon
: st::boostsListGiftMiniIcon;
_rightBadge = (_boost.isGift || _boost.isGiveaway)
? CreateBadge(
st::boostsListRightBadgeTextStyle,
_boost.isGiveaway
? tr::lng_gift_link_reason_giveaway(tr::now)
: tr::lng_gift_link_label_gift(tr::now),
st::boostsListRightBadgeHeight,
st::boostsListRightBadgeTextPadding,
rightColor,
rightColor,
kBadgeBgOpacity,
st::boostsListGiftMiniIconPadding,
rightIcon)
: QImage();
}
QSize BoostRow::rightActionSize() const {
return _rightBadge.size() / style::DevicePixelRatio();
}
QMargins BoostRow::rightActionMargins() const {
return st::boostsListRightBadgePadding;
}
bool BoostRow::rightActionDisabled() const {
return true;
}
void BoostRow::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
if (!_rightBadge.isNull()) {
p.drawImage(x, y, _rightBadge);
}
}
int BoostRow::paintNameIconGetWidth(
Painter &p,
Fn<void()> repaint,
crl::time now,
int nameLeft,
int nameTop,
int nameWidth,
int availableWidth,
int outerWidth,
bool selected) {
if (_badge.isNull()) {
return 0;
}
const auto badgew = _badge.width() / style::DevicePixelRatio();
const auto nameTooLarge = (nameWidth > availableWidth);
const auto &padding = st::boostsListBadgePadding;
const auto left = nameTooLarge
? ((nameLeft + availableWidth) - badgew - padding.left())
: (nameLeft + nameWidth + padding.right());
p.drawImage(left, nameTop + padding.top(), _badge);
return badgew + (nameTooLarge ? padding.left() : 0);
}
class BoostsController final : public PeerListController {
public:
explicit BoostsController(BoostsDescriptor d);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
[[nodiscard]] bool skipRequest() const;
void requestNext();
[[nodiscard]] rpl::producer<int> totalBoostsValue() const;
private:
void applySlice(const Data::BoostsListSlice &slice);
const not_null<Main::Session*> _session;
BoostCallback _boostClickedCallback;
Api::Boosts _api;
Data::BoostsListSlice _firstSlice;
Data::BoostsListSlice::OffsetToken _apiToken;
bool _allLoaded = false;
bool _requesting = false;
rpl::variable<int> _totalBoosts;
};
BoostsController::BoostsController(BoostsDescriptor d)
: _session(&d.peer->session())
, _boostClickedCallback(std::move(d.boostClickedCallback))
, _api(d.peer)
, _firstSlice(std::move(d.firstSlice)) {
PeerListController::setStyleOverrides(&st::boostsListBox);
}
Main::Session &BoostsController::session() const {
return *_session;
}
bool BoostsController::skipRequest() const {
return _requesting || _allLoaded;
}
void BoostsController::requestNext() {
_requesting = true;
_api.requestBoosts(_apiToken, [=](const Data::BoostsListSlice &slice) {
_requesting = false;
applySlice(slice);
});
}
void BoostsController::prepare() {
applySlice(base::take(_firstSlice));
delegate()->peerListRefreshRows();
}
void BoostsController::loadMoreRows() {
}
void BoostsController::applySlice(const Data::BoostsListSlice &slice) {
_allLoaded = slice.allLoaded;
_apiToken = slice.token;
auto sumFromSlice = 0;
for (const auto &item : slice.list) {
sumFromSlice += item.multiplier ? item.multiplier : 1;
auto row = [&] {
if (item.userId && !item.isUnclaimed) {
const auto user = session().data().user(item.userId);
return std::make_unique<BoostRow>(user, item);
} else {
return std::make_unique<BoostRow>(item);
}
}();
delegate()->peerListAppendRow(std::move(row));
}
delegate()->peerListRefreshRows();
_totalBoosts = _totalBoosts.current() + sumFromSlice;
}
void BoostsController::rowClicked(not_null<PeerListRow*> row) {
if (_boostClickedCallback) {
_boostClickedCallback(
static_cast<const BoostRow*>(row.get())->boost());
}
}
rpl::producer<int> BoostsController::totalBoostsValue() const {
return _totalBoosts.value();
}
} // namespace
void AddPublicForwards(
const Data::PublicForwardsSlice &firstSlice,
not_null<Ui::VerticalLayout*> container,
Fn<void(Data::RecentPostId)> requestShow,
not_null<PeerData*> peer,
Data::RecentPostId contextId) {
if (!peer->isChannel()) {
return;
}
struct State final {
State(PublicForwardsDescriptor d) : controller(std::move(d)) {
}
PeerListContentDelegateSimple delegate;
PublicForwardsController controller;
};
auto d = PublicForwardsDescriptor{
firstSlice,
std::move(requestShow),
peer,
contextId,
};
const auto state = container->lifetime().make_state<State>(std::move(d));
if (const auto total = firstSlice.total; total > 0) {
AddSubtitle(
container,
tr::lng_stats_overview_message_public_share(
lt_count_decimal,
rpl::single<float64>(total)));
}
state->delegate.setContent(container->add(
object_ptr<PeerListContent>(container, &state->controller)));
state->controller.setDelegate(&state->delegate);
}
void AddMembersList(
Data::SupergroupStatistics data,
not_null<Ui::VerticalLayout*> container,
Fn<void(not_null<PeerData*>)> showPeerInfo,
not_null<PeerData*> peer,
rpl::producer<QString> title) {
if (!peer->isMegagroup()) {
return;
}
const auto max = !data.topSenders.empty()
? data.topSenders.size()
: !data.topAdministrators.empty()
? data.topAdministrators.size()
: !data.topInviters.empty()
? data.topInviters.size()
: 0;
if (!max) {
return;
}
constexpr auto kPerPage = 40;
struct State final {
State(MembersDescriptor d) : controller(std::move(d)) {
}
PeerListContentDelegateSimple delegate;
MembersController controller;
int limit = 0;
};
auto d = MembersDescriptor{
&peer->session(),
std::move(showPeerInfo),
std::move(data),
};
const auto state = container->lifetime().make_state<State>(std::move(d));
AddSubtitle(container, std::move(title));
state->delegate.setContent(container->add(
object_ptr<PeerListContent>(container, &state->controller)));
state->controller.setDelegate(&state->delegate);
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
object_ptr<Ui::SettingsButton>(
container,
tr::lng_stories_show_more())),
{ 0, -st::settingsButton.padding.top(), 0, 0 });
const auto showMore = [=] {
state->limit = std::min(int(max), state->limit + kPerPage);
state->controller.setLimit(state->limit);
if (state->limit == max) {
wrap->toggle(false, anim::type::instant);
}
container->resizeToWidth(container->width());
};
wrap->entity()->setClickedCallback(showMore);
showMore();
}
void AddBoostsList(
const Data::BoostsListSlice &firstSlice,
not_null<Ui::VerticalLayout*> container,
BoostCallback boostClickedCallback,
not_null<PeerData*> peer,
rpl::producer<QString> title) {
const auto max = firstSlice.multipliedTotal;
struct State final {
State(BoostsDescriptor d) : controller(std::move(d)) {
}
PeerListContentDelegateSimple delegate;
BoostsController controller;
};
auto d = BoostsDescriptor{ firstSlice, boostClickedCallback, peer };
const auto state = container->lifetime().make_state<State>(std::move(d));
state->delegate.setContent(container->add(
object_ptr<PeerListContent>(container, &state->controller)));
state->controller.setDelegate(&state->delegate);
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
object_ptr<Ui::SettingsButton>(
container,
(firstSlice.token.gifts
? tr::lng_boosts_show_more_gifts
: tr::lng_boosts_show_more_boosts)(
lt_count,
state->controller.totalBoostsValue(
) | rpl::map(
max - rpl::mappers::_1
) | tr::to_count()),
st::statisticsShowMoreButton)),
{ 0, -st::settingsButton.padding.top(), 0, 0 });
const auto button = wrap->entity();
AddArrow(button);
const auto showMore = [=] {
if (!state->controller.skipRequest()) {
state->controller.requestNext();
container->resizeToWidth(container->width());
}
};
wrap->toggleOn(
state->controller.totalBoostsValue(
) | rpl::map(rpl::mappers::_1 > 0 && rpl::mappers::_1 < max),
anim::type::instant);
button->setClickedCallback(showMore);
}
} // namespace Info::Statistics