tdesktop/Telegram/SourceFiles/info/polls/info_polls_results_inner_wi...

683 lines
18 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/polls/info_polls_results_inner_widget.h"
#include "info/polls/info_polls_results_widget.h"
#include "info/info_controller.h"
#include "lang/lang_keys.h"
#include "data/data_poll.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "ui/controls/peer_list_dummy.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/text/text_utilities.h"
#include "boxes/peer_list_box.h"
#include "main/main_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
namespace Info {
namespace Polls {
namespace {
constexpr auto kFirstPage = 15;
constexpr auto kPerPage = 50;
constexpr auto kLeavePreloaded = 5;
class ListDelegate final : public PeerListContentDelegate {
public:
void peerListSetTitle(rpl::producer<QString> title) override;
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
int peerListSelectedRowsCount() override;
void peerListScrollToTop() override;
void peerListAddSelectedPeerInBunch(
not_null<PeerData*> peer) override;
void peerListAddSelectedRowInBunch(
not_null<PeerListRow*> row) override;
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
void peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options = Ui::LayerOption::KeepOther) override;
void peerListHideLayer() override;
not_null<QWidget*> peerListToastParent() override;
};
void ListDelegate::peerListSetTitle(rpl::producer<QString> title) {
}
void ListDelegate::peerListSetAdditionalTitle(rpl::producer<QString> title) {
}
bool ListDelegate::peerListIsRowChecked(not_null<PeerListRow*> row) {
return false;
}
int ListDelegate::peerListSelectedRowsCount() {
return 0;
}
void ListDelegate::peerListScrollToTop() {
}
void ListDelegate::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {
Unexpected("Item selection in Info::Profile::Members.");
}
void ListDelegate::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {
Unexpected("Item selection in Info::Profile::Members.");
}
void ListDelegate::peerListFinishSelectedRowsBunch() {
}
void ListDelegate::peerListSetDescription(
object_ptr<Ui::FlatLabel> description) {
description.destroy();
}
void ListDelegate::peerListShowBox(
object_ptr<Ui::BoxContent> content,
Ui::LayerOptions options) {
}
void ListDelegate::peerListHideLayer() {
}
not_null<QWidget*> ListDelegate::peerListToastParent() {
Unexpected("...ListDelegate::peerListToastParent");
}
} // namespace
class ListController final : public PeerListController {
public:
ListController(
not_null<Main::Session*> session,
not_null<PollData*> poll,
FullMsgId context,
QByteArray option);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
void allowLoadMore();
void collapse();
[[nodiscard]] auto showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>>;
[[nodiscard]] rpl::producer<int> scrollToRequests() const;
[[nodiscard]] rpl::producer<int> count() const;
[[nodiscard]] rpl::producer<int> fullCount() const;
[[nodiscard]] rpl::producer<int> loadMoreCount() const;
std::unique_ptr<PeerListState> saveState() const override;
void restoreState(std::unique_ptr<PeerListState> state) override;
std::unique_ptr<PeerListRow> createRestoredRow(
not_null<PeerData*> peer) override;
void scrollTo(int y);
private:
struct SavedState : SavedStateBase {
QString offset;
QString loadForOffset;
int leftToLoad = 0;
int fullCount = 0;
std::vector<not_null<UserData*>> preloaded;
bool wasLoading = false;
};
bool appendRow(not_null<UserData*> user);
std::unique_ptr<PeerListRow> createRow(not_null<UserData*> user) const;
void addPreloaded();
bool addPreloadedPage();
void preloadedAdded();
const not_null<Main::Session*> _session;
const not_null<PollData*> _poll;
const FullMsgId _context;
const QByteArray _option;
MTP::Sender _api;
QString _offset;
mtpRequestId _loadRequestId = 0;
QString _loadForOffset;
std::vector<not_null<UserData*>> _preloaded;
rpl::variable<int> _count = 0;
rpl::variable<int> _fullCount;
rpl::variable<int> _leftToLoad;
rpl::event_stream<not_null<PeerData*>> _showPeerInfoRequests;
rpl::event_stream<int> _scrollToRequests;
};
ListController::ListController(
not_null<Main::Session*> session,
not_null<PollData*> poll,
FullMsgId context,
QByteArray option)
: _session(session)
, _poll(poll)
, _context(context)
, _option(option)
, _api(&_session->mtp()) {
const auto i = ranges::find(poll->answers, option, &PollAnswer::option);
Assert(i != poll->answers.end());
_fullCount = i->votes;
_leftToLoad = i->votes;
}
Main::Session &ListController::session() const {
return *_session;
}
void ListController::prepare() {
delegate()->peerListRefreshRows();
}
void ListController::loadMoreRows() {
if (_loadRequestId
|| !_leftToLoad.current()
|| (!_offset.isEmpty() && _loadForOffset != _offset)
|| !_preloaded.empty()) {
return;
}
const auto item = session().data().message(_context);
if (!item || !item->isRegular()) {
_leftToLoad = 0;
return;
}
using Flag = MTPmessages_GetPollVotes::Flag;
const auto flags = Flag::f_option
| (_offset.isEmpty() ? Flag(0) : Flag::f_offset);
const auto limit = _offset.isEmpty() ? kFirstPage : kPerPage;
_loadRequestId = _api.request(MTPmessages_GetPollVotes(
MTP_flags(flags),
item->history()->peer->input,
MTP_int(item->id),
MTP_bytes(_option),
MTP_string(_offset),
MTP_int(limit)
)).done([=](const MTPmessages_VotesList &result) {
const auto count = result.match([&](
const MTPDmessages_votesList &data) {
_offset = data.vnext_offset().value_or_empty();
auto &owner = session().data();
owner.processUsers(data.vusers());
auto add = limit - kLeavePreloaded;
for (const auto &vote : data.vvotes().v) {
vote.match([&](const auto &data) {
const auto user = owner.user(data.vuser_id().v);
if (user->isMinimalLoaded()) {
if (add) {
appendRow(user);
--add;
} else {
_preloaded.push_back(user);
}
}
});
}
return data.vcount().v;
});
if (_offset.isEmpty()) {
addPreloaded();
_fullCount = delegate()->peerListFullRowsCount();
_leftToLoad = 0;
} else {
_count = delegate()->peerListFullRowsCount();
_fullCount = count;
_leftToLoad = count - delegate()->peerListFullRowsCount();
delegate()->peerListRefreshRows();
}
_loadRequestId = 0;
}).fail([=] {
_loadRequestId = 0;
}).send();
}
void ListController::allowLoadMore() {
if (!addPreloadedPage()) {
_loadForOffset = _offset;
addPreloaded();
loadMoreRows();
}
}
void ListController::collapse() {
const auto count = delegate()->peerListFullRowsCount();
if (count <= kFirstPage) {
return;
}
const auto remove = count - (kFirstPage - kLeavePreloaded);
ranges::actions::reverse(_preloaded);
_preloaded.reserve(_preloaded.size() + remove);
for (auto i = 0; i != remove; ++i) {
const auto row = delegate()->peerListRowAt(count - i - 1);
_preloaded.push_back(row->peer()->asUser());
delegate()->peerListRemoveRow(row);
}
ranges::actions::reverse(_preloaded);
delegate()->peerListRefreshRows();
const auto now = count - remove;
_count = now;
_leftToLoad = _fullCount.current() - now;
}
void ListController::addPreloaded() {
for (const auto user : base::take(_preloaded)) {
appendRow(user);
}
preloadedAdded();
}
bool ListController::addPreloadedPage() {
if (_preloaded.size() < kPerPage + kLeavePreloaded) {
return false;
}
const auto from = begin(_preloaded);
const auto till = from + kPerPage;
for (auto i = from; i != till; ++i) {
appendRow(*i);
}
_preloaded.erase(from, till);
preloadedAdded();
return true;
}
void ListController::preloadedAdded() {
_count = delegate()->peerListFullRowsCount();
_leftToLoad = _fullCount.current() - _count.current();
delegate()->peerListRefreshRows();
}
auto ListController::showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>> {
return _showPeerInfoRequests.events();
}
rpl::producer<int> ListController::scrollToRequests() const {
return _scrollToRequests.events();
}
rpl::producer<int> ListController::count() const {
return _count.value();
}
rpl::producer<int> ListController::fullCount() const {
return _fullCount.value();
}
rpl::producer<int> ListController::loadMoreCount() const {
const auto initial = (_fullCount.current() <= kFirstPage)
? _fullCount.current()
: (kFirstPage - kLeavePreloaded);
return rpl::combine(
_count.value(),
_leftToLoad.value()
) | rpl::map([=](int count, int leftToLoad) {
return (count > 0) ? leftToLoad : (leftToLoad - initial);
});
}
auto ListController::saveState() const -> std::unique_ptr<PeerListState> {
auto result = PeerListController::saveState();
auto my = std::make_unique<SavedState>();
my->offset = _offset;
my->fullCount = _fullCount.current();
my->leftToLoad = _leftToLoad.current();
my->preloaded = _preloaded;
my->wasLoading = (_loadRequestId != 0);
my->loadForOffset = _loadForOffset;
result->controllerState = std::move(my);
return result;
}
void ListController::restoreState(std::unique_ptr<PeerListState> state) {
auto typeErasedState = state
? state->controllerState.get()
: nullptr;
if (const auto my = dynamic_cast<SavedState*>(typeErasedState)) {
if (const auto requestId = base::take(_loadRequestId)) {
_api.request(requestId).cancel();
}
_offset = my->offset;
_loadForOffset = my->loadForOffset;
_preloaded = std::move(my->preloaded);
_count = int(state->list.size());
_fullCount = my->fullCount;
_leftToLoad = my->leftToLoad;
if (my->wasLoading) {
loadMoreRows();
}
PeerListController::restoreState(std::move(state));
}
}
std::unique_ptr<PeerListRow> ListController::createRestoredRow(
not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
return createRow(user);
}
return nullptr;
}
void ListController::rowClicked(not_null<PeerListRow*> row) {
_showPeerInfoRequests.fire(row->peer());
}
bool ListController::appendRow(not_null<UserData*> user) {
if (delegate()->peerListFindRow(user->id.value)) {
return false;
}
delegate()->peerListAppendRow(createRow(user));
return true;
}
std::unique_ptr<PeerListRow> ListController::createRow(
not_null<UserData*> user) const {
auto row = std::make_unique<PeerListRow>(user);
row->setCustomStatus(QString());
return row;
}
void ListController::scrollTo(int y) {
_scrollToRequests.fire_copy(y);
}
ListController *CreateAnswerRows(
not_null<Ui::VerticalLayout*> container,
rpl::producer<int> visibleTop,
not_null<Main::Session*> session,
not_null<PollData*> poll,
FullMsgId context,
const PollAnswer &answer) {
using namespace rpl::mappers;
if (!answer.votes) {
return nullptr;
}
const auto delegate = container->lifetime().make_state<ListDelegate>();
const auto controller = container->lifetime().make_state<ListController>(
session,
poll,
context,
answer.option);
const auto percent = answer.votes * 100 / poll->totalVoters;
const auto phrase = poll->quiz()
? tr::lng_polls_answers_count
: tr::lng_polls_votes_count;
const auto sampleText = phrase(
tr::now,
lt_count_decimal,
answer.votes);
const auto &font = st::boxDividerLabel.style.font;
const auto sampleWidth = font->width(sampleText);
const auto rightSkip = sampleWidth + font->spacew * 4;
const auto headerWrap = container->add(
object_ptr<Ui::RpWidget>(
container));
container->add(object_ptr<Ui::FixedHeightWidget>(
container,
st::boxLittleSkip));
controller->setStyleOverrides(&st::infoCommonGroupsList);
const auto content = container->add(object_ptr<PeerListContent>(
container,
controller));
delegate->setContent(content);
controller->setDelegate(delegate);
const auto count = (answer.votes <= kFirstPage)
? answer.votes
: (kFirstPage - kLeavePreloaded);
const auto placeholder = container->add(object_ptr<PeerListDummy>(
container,
count,
st::infoCommonGroupsList));
controller->count(
) | rpl::filter(_1 > 0) | rpl::start_with_next([=] {
delete placeholder;
}, placeholder->lifetime());
const auto header = Ui::CreateChild<Ui::DividerLabel>(
container.get(),
object_ptr<Ui::FlatLabel>(
container,
(answer.text
+ QString::fromUtf8(" \xe2\x80\x94 ")
+ QString::number(percent)
+ "%"),
st::boxDividerLabel),
style::margins(
st::pollResultsHeaderPadding.left(),
st::pollResultsHeaderPadding.top(),
st::pollResultsHeaderPadding.right() + rightSkip,
st::pollResultsHeaderPadding.bottom()));
const auto votes = Ui::CreateChild<Ui::FlatLabel>(
header,
phrase(
lt_count_decimal,
controller->fullCount() | rpl::map(_1 + 0.)),
st::pollResultsVotesCount);
const auto collapse = Ui::CreateChild<Ui::LinkButton>(
header,
tr::lng_polls_votes_collapse(tr::now),
st::defaultLinkButton);
collapse->setClickedCallback([=] {
controller->scrollTo(headerWrap->y());
controller->collapse();
});
rpl::combine(
controller->fullCount(),
controller->count()
) | rpl::start_with_next([=](int fullCount, int count) {
const auto many = (fullCount > kFirstPage)
&& (count > kFirstPage - kLeavePreloaded);
collapse->setVisible(many);
votes->setVisible(!many);
}, collapse->lifetime());
headerWrap->widthValue(
) | rpl::start_with_next([=](int width) {
header->resizeToWidth(width);
votes->moveToRight(
st::pollResultsHeaderPadding.right(),
st::pollResultsHeaderPadding.top(),
width);
collapse->moveToRight(
st::pollResultsHeaderPadding.right(),
st::pollResultsHeaderPadding.top(),
width);
}, header->lifetime());
header->heightValue(
) | rpl::start_with_next([=](int height) {
headerWrap->resize(headerWrap->width(), height);
}, header->lifetime());
auto moreTopWidget = object_ptr<Ui::RpWidget>(container);
moreTopWidget->resize(0, 0);
const auto moreTop = container->add(std::move(moreTopWidget));
const auto more = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
object_ptr<Ui::SettingsButton>(
container,
tr::lng_polls_show_more(
lt_count_decimal,
controller->loadMoreCount() | rpl::map(_1 + 0.),
Ui::Text::Upper),
st::pollResultsShowMore)));
more->entity()->setClickedCallback([=] {
controller->allowLoadMore();
});
controller->loadMoreCount(
) | rpl::map(_1 > 0) | rpl::start_with_next([=](bool visible) {
more->toggle(visible, anim::type::instant);
}, more->lifetime());
container->add(object_ptr<Ui::FixedHeightWidget>(
container,
st::boxLittleSkip));
rpl::combine(
std::move(visibleTop),
headerWrap->geometryValue(),
moreTop->topValue()
) | rpl::filter([=](int, QRect headerRect, int moreTop) {
return moreTop >= headerRect.y() + headerRect.height();
}) | rpl::start_with_next([=](
int visibleTop,
QRect headerRect,
int moreTop) {
const auto skip = st::pollResultsHeaderPadding.top()
- st::pollResultsHeaderPadding.bottom();
const auto top = std::clamp(
visibleTop - skip,
headerRect.y(),
moreTop - headerRect.height());
header->move(0, top);
}, header->lifetime());
return controller;
}
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PollData*> poll,
FullMsgId contextId)
: RpWidget(parent)
, _controller(controller)
, _poll(poll)
, _contextId(contextId)
, _content(this) {
setupContent();
}
void InnerWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_content, visibleTop, visibleBottom);
_visibleTop = visibleTop;
}
void InnerWidget::saveState(not_null<Memento*> memento) {
auto states = base::flat_map<
QByteArray,
std::unique_ptr<PeerListState>>();
for (const auto &[option, controller] : _sections) {
states[option] = controller->saveState();
}
memento->setListStates(std::move(states));
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
auto states = memento->listStates();
for (const auto &[option, controller] : _sections) {
const auto i = states.find(option);
if (i != end(states)) {
controller->restoreState(std::move(i->second));
}
}
}
int InnerWidget::desiredHeight() const {
auto desired = 0;
//auto count = qMax(_user->commonChatsCount(), 1);
//desired += qMax(count, _list->fullRowsCount())
// * st::infoCommonGroupsList.item.height;
return qMax(height(), desired);
}
void InnerWidget::setupContent() {
_content->add(
object_ptr<Ui::FlatLabel>(
_content,
_poll->question,
st::pollResultsQuestion),
style::margins{
st::boxRowPadding.left(),
0,
st::boxRowPadding.right(),
st::boxMediumSkip });
for (const auto &answer : _poll->answers) {
const auto session = &_controller->session();
const auto controller = CreateAnswerRows(
_content,
_visibleTop.value(),
session,
_poll,
_contextId,
answer);
if (!controller) {
continue;
}
controller->showPeerInfoRequests(
) | rpl::start_to_stream(
_showPeerInfoRequests,
lifetime());
controller->scrollToRequests(
) | rpl::start_with_next([=](int y) {
_scrollToRequests.fire({ y, -1 });
}, lifetime());
_sections.emplace(answer.option, controller);
}
widthValue(
) | rpl::start_with_next([=](int newWidth) {
_content->resizeToWidth(newWidth);
}, _content->lifetime());
_content->heightValue(
) | rpl::start_with_next([=](int height) {
resize(width(), height);
}, _content->lifetime());
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
auto InnerWidget::showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>> {
return _showPeerInfoRequests.events();
}
} // namespace Polls
} // namespace Info