diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1cc4c04b89..55555727c7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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}"; diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 733127a910..363b482f50 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -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 item) { _pollReloadRequestIds.emplace(itemId, requestId); } +void Polls::requestStats( + FullMsgId itemId, + Fn done, + Fn 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 diff --git a/Telegram/SourceFiles/api/api_polls.h b/Telegram/SourceFiles/api/api_polls.h index 26d46e09af..a7b1491d5f 100644 --- a/Telegram/SourceFiles/api/api_polls.h +++ b/Telegram/SourceFiles/api/api_polls.h @@ -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 item); void reloadResults(not_null item); + void requestStats( + FullMsgId itemId, + Fn done, + Fn fail); private: const not_null _session; diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index 23e046aa54..11b167240a 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -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'; } diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index d3059639ce..be839ca0e1 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -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; @@ -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; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 3bc816d385..234e7e7bf8 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -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() diff --git a/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.cpp b/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.cpp index 2a3774d75c..111a67d3be 100644 --- a/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.cpp +++ b/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.cpp @@ -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 controller, + FullMsgId itemId) { + controller->show(Box([=](not_null box) { + box->setTitle(tr::lng_polls_stats_title()); + box->setWidth(st::boxWideWidth); + + const auto content = box->addRow( + object_ptr(box), + Margins(0)); + const auto loadingWrap = content->add( + object_ptr>( + content, + object_ptr(content))); + loadingWrap->toggle(true, anim::type::instant); + const auto loading = loadingWrap->entity(); + const auto resultWrap = content->add( + object_ptr>( + content, + object_ptr(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( + loading, + tr::lng_stats_loading(), + st::changePhoneTitle), + st::changePhoneTitlePadding + st::boxRowPadding, + style::al_top); + loading->add( + object_ptr( + 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( + 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(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( diff --git a/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.h b/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.h index 8b5765c789..32709bb61f 100644 --- a/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.h +++ b/Telegram/SourceFiles/history/view/media/menu/history_view_poll_menu.h @@ -52,6 +52,10 @@ void FillPollAnswerMenu( FullMsgId itemId, not_null controller); +void ShowPollStatsBox( + not_null controller, + FullMsgId itemId); + void ShowPollStickerPreview( not_null controller, not_null document,