Added poll stats action and dialog with API-backed chart loading.

This commit is contained in:
23rd
2026-04-09 11:44:37 +03:00
committed by John Preston
parent 390fec44b6
commit 1773f264eb
8 changed files with 200 additions and 1 deletions
+2
View File
@@ -6947,7 +6947,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_polls_answers_count#other" = "{count} answers";
"lng_polls_answers_none" = "No answers";
"lng_polls_submit_votes" = "Vote";
"lng_polls_view_stats" = "View Stats";
"lng_polls_view_results" = "View results";
"lng_polls_stats_title" = "Poll Stats";
"lng_polls_view_votes#one" = "View Votes ({count})";
"lng_polls_view_votes#other" = "View Votes ({count})";
"lng_polls_admin_votes#one" = "{count} vote {arrow}";
+48
View File
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_polls.h"
#include "api/api_common.h"
#include "api/api_statistics_data_deserialize.h"
#include "api/api_text_entities.h"
#include "api/api_updates.h"
#include "apiwrap.h"
@@ -18,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_histories.h"
#include "data/data_poll.h"
#include "data/data_session.h"
#include "data/data_statistics_chart.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h" // ShouldSendSilent
@@ -473,4 +475,50 @@ void Polls::reloadResults(not_null<HistoryItem*> item) {
_pollReloadRequestIds.emplace(itemId, requestId);
}
void Polls::requestStats(
FullMsgId itemId,
Fn<void(Data::StatisticalGraph)> done,
Fn<void(QString)> fail) {
const auto item = _session->data().message(itemId);
if (!item || !item->isRegular()) {
if (fail) {
fail(QString());
}
return;
}
const auto requestGraph = [=](const QString &token) {
_api.request(MTPstats_LoadAsyncGraph(
MTP_flags(MTPstats_LoadAsyncGraph::Flag(0)),
MTP_string(token),
MTP_long(0)
)).done([=](const MTPStatsGraph &result) {
if (done) {
done(Api::StatisticalGraphFromTL(result));
}
}).fail([=](const MTP::Error &error) {
if (fail) {
fail(error.type());
}
}).send();
};
_api.request(MTPstats_GetPollStats(
MTP_flags(MTPstats_GetPollStats::Flags(0)),
item->history()->peer->input(),
MTP_int(item->id)
)).done([=](const MTPstats_PollStats &result) {
auto graph = Api::StatisticalGraphFromTL(result.data().vvotes_graph());
if (graph.chart || !graph.error.isEmpty() || graph.zoomToken.isEmpty()) {
if (done) {
done(std::move(graph));
}
} else {
requestGraph(graph.zoomToken);
}
}).fail([=](const MTP::Error &error) {
if (fail) {
fail(error.type());
}
}).send();
}
} // namespace Api
+7
View File
@@ -14,6 +14,9 @@ class ApiWrap;
class HistoryItem;
struct PollData;
struct PollMedia;
namespace Data {
struct StatisticalGraph;
} // namespace Data
namespace Main {
class Session;
@@ -45,6 +48,10 @@ public:
void deleteAnswer(FullMsgId itemId, const QByteArray &option);
void close(not_null<HistoryItem*> item);
void reloadResults(not_null<HistoryItem*> item);
void requestStats(
FullMsgId itemId,
Fn<void(Data::StatisticalGraph)> done,
Fn<void(QString)> fail);
private:
const not_null<Main::Session*> _session;
+22
View File
@@ -188,6 +188,21 @@ bool PollData::applyResults(const MTPPollResults &results) {
const auto newTotalVoters
= results.vtotal_voters().value_or(totalVoters);
auto changed = (newTotalVoters != totalVoters);
const auto setCanViewStats = [&](bool value) {
const auto previous = (_flags & Flag::CanViewStats);
if (previous == value) {
return;
}
if (value) {
_flags |= Flag::CanViewStats;
} else {
_flags &= ~Flag::CanViewStats;
}
changed = true;
};
if (!results.is_min() || results.is_can_view_stats()) {
setCanViewStats(results.is_can_view_stats());
}
if (const auto list = results.vresults()) {
for (const auto &result : list->v) {
if (applyResultToAnswers(result, results.is_min())) {
@@ -382,6 +397,10 @@ bool PollData::subscribersOnly() const {
return (_flags & Flag::SubscribersOnly);
}
bool PollData::canViewStats() const {
return (_flags & Flag::CanViewStats);
}
QString PollData::debugString() const {
auto result = QString();
result += u"Poll #"_q + QString::number(id) + u'\n';
@@ -401,6 +420,9 @@ QString PollData::debugString() const {
if (subscribersOnly()) {
result += u"[SubscribersOnly]"_q;
}
if (canViewStats()) {
result += u"[CanViewStats]"_q;
}
if (!result.endsWith(u'\n')) {
result += u'\n';
}
+2
View File
@@ -77,6 +77,7 @@ struct PollData {
HideResultsUntilClose = 0x080,
Creator = 0x100,
SubscribersOnly = 0x200,
CanViewStats = 0x400,
};
friend inline constexpr bool is_flag_type(Flag) { return true; };
using Flags = base::flags<Flag>;
@@ -103,6 +104,7 @@ struct PollData {
[[nodiscard]] bool hideResultsUntilClose() const;
[[nodiscard]] bool creator() const;
[[nodiscard]] bool subscribersOnly() const;
[[nodiscard]] bool canViewStats() const;
[[nodiscard]] QString debugString() const;
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item_text.h"
#include "history/view/history_view_schedule_box.h"
#include "history/view/media/history_view_media.h"
#include "history/view/media/menu/history_view_poll_menu.h"
#include "history/view/media/history_view_save_document_action.h"
#include "history/view/media/history_view_web_page.h"
#include "history/view/reactions/history_view_reactions_list.h"
@@ -1891,10 +1892,15 @@ void AddPollActions(
&& (context != Context::ChatPreview)) {
return;
}
const auto itemId = item->fullId();
if (poll->canViewStats() && item->isRegular()) {
menu->addAction(tr::lng_polls_view_stats(tr::now), [=] {
ShowPollStatsBox(controller, itemId);
}, &st::menuIconStats);
}
if (poll->closed()) {
return;
}
const auto itemId = item->fullId();
if (!skipRetractVote
&& poll->voted()
&& !poll->quiz()
@@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_photo_media.h"
#include "data/data_poll.h"
#include "data/data_session.h"
#include "data/data_statistics_chart.h"
#include "data/stickers/data_stickers.h"
#include "editor/editor_layer_widget.h"
#include "editor/photo_editor.h"
@@ -29,21 +30,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_document.h"
#include "lang/lang_keys.h"
#include "layout/layout_document_generic_preview.h"
#include "lottie/lottie_icon.h"
#include "main/main_session.h"
#include "poll/poll_media_upload.h"
#include "statistics/chart_widget.h"
#include "mainwidget.h"
#include "settings/settings_common.h"
#include "storage/localimageloader.h"
#include "storage/storage_media_prepare.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/text/format_song_name.h"
#include "ui/text/format_values.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/slide_wrap.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
@@ -51,6 +59,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_layers.h"
#include "styles/style_media_view.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include "styles/style_statistics.h"
#include "styles/style_widgets.h"
namespace HistoryView {
@@ -249,6 +259,104 @@ void FillPollAnswerMenu(
}
}
void ShowPollStatsBox(
not_null<Window::SessionController*> controller,
FullMsgId itemId) {
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_polls_stats_title());
box->setWidth(st::boxWideWidth);
const auto content = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
Margins(0));
const auto loadingWrap = content->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
content,
object_ptr<Ui::VerticalLayout>(content)));
loadingWrap->toggle(true, anim::type::instant);
const auto loading = loadingWrap->entity();
const auto resultWrap = content->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
content,
object_ptr<Ui::VerticalLayout>(content)));
resultWrap->toggle(false, anim::type::instant);
const auto result = resultWrap->entity();
auto icon = ::Settings::CreateLottieIcon(
loading,
{ .name = u"stats"_q, .sizeOverride = st::normalBoxLottieSize },
st::settingsBlockedListIconPadding);
loading->add(std::move(icon.widget));
auto startAnimation = std::move(icon.animate);
box->showFinishes(
) | rpl::take(1) | rpl::on_next([=]() mutable {
startAnimation(anim::repeat::loop);
}, loading->lifetime());
loading->add(
object_ptr<Ui::FlatLabel>(
loading,
tr::lng_stats_loading(),
st::changePhoneTitle),
st::changePhoneTitlePadding + st::boxRowPadding,
style::al_top);
loading->add(
object_ptr<Ui::FlatLabel>(
loading,
tr::lng_stats_loading_subtext(),
st::statisticsLoadingSubtext),
st::changePhoneDescriptionPadding + st::boxRowPadding,
style::al_top
)->setTryMakeSimilarLines(true);
Ui::AddSkip(loading, st::settingsBlockedListIconPadding.top());
const auto finishLoading = [=] {
loading->clear();
loadingWrap->toggle(false, anim::type::instant);
resultWrap->toggle(true, anim::type::instant);
};
const auto showError = [=](QString error) {
finishLoading();
result->clear();
result->add(
object_ptr<Ui::FlatLabel>(
result,
error.isEmpty()
? tr::lng_polls_votes_none(tr::now)
: std::move(error),
st::defaultFlatLabel),
st::boxRowPadding + st::statisticsLayerMargins,
style::al_center);
};
controller->session().api().polls().requestStats(
itemId,
crl::guard(box, [=](Data::StatisticalGraph graph) {
if (graph.chart) {
finishLoading();
result->clear();
const auto chart = result->add(
object_ptr<Statistic::ChartWidget>(result),
st::statisticsLayerMargins);
chart->setChartData(
std::move(graph.chart),
Statistic::ChartViewType::Linear);
chart->setTitle(tr::lng_notification_reactions_poll_votes());
Statistic::FixCacheForHighDPIChartWidget(result);
} else {
showError(!graph.error.isEmpty()
? graph.error
: tr::lng_polls_votes_none(tr::now));
}
}),
crl::guard(box, [=](QString error) {
showError(std::move(error));
}));
box->addButton(tr::lng_box_ok(), [=] {
box->closeBox();
});
}));
}
namespace {
void AddRemoveAction(
@@ -52,6 +52,10 @@ void FillPollAnswerMenu(
FullMsgId itemId,
not_null<Window::SessionController*> controller);
void ShowPollStatsBox(
not_null<Window::SessionController*> controller,
FullMsgId itemId);
void ShowPollStickerPreview(
not_null<Window::SessionController*> controller,
not_null<DocumentData*> document,