From bbdc6abea859ed8512723ca18136f65d5b744957 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 18 Mar 2026 17:22:09 +0300 Subject: [PATCH] Added recent voters to media view. --- Telegram/SourceFiles/data/data_poll.cpp | 24 +++ Telegram/SourceFiles/data/data_poll.h | 3 + Telegram/SourceFiles/data/data_session.cpp | 5 + Telegram/SourceFiles/data/data_session.h | 2 + .../SourceFiles/media/view/media_view.style | 9 ++ .../media/view/media_view_overlay_widget.cpp | 146 +++++++++++++++++- .../media/view/media_view_overlay_widget.h | 4 + 7 files changed, 186 insertions(+), 7 deletions(-) diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index a0bba867bf..684592a547 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -276,6 +276,30 @@ bool PollData::applyResultToAnswers( answer->correct = voters.is_correct(); changed = true; } + if (const auto recent = voters.vrecent_voters()) { + const auto recentChanged = !ranges::equal( + answer->recentVoters, + recent->v, + ranges::equal_to(), + &PeerData::id, + peerFromMTP); + if (recentChanged) { + changed = true; + answer->recentVoters = ranges::views::all( + recent->v + ) | ranges::views::transform([&](MTPPeer peerId) { + const auto peer = _owner->peer( + peerFromMTP(peerId)); + return peer->isMinimalLoaded() + ? peer.get() + : nullptr; + }) | ranges::views::filter([](PeerData *peer) { + return peer != nullptr; + }) | ranges::views::transform([](PeerData *peer) { + return not_null(peer); + }) | ranges::to_vector; + } + } return changed; }); } diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index 1994e00c0d..c105b1eb49 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -15,10 +15,13 @@ namespace Main { class Session; } // namespace Main +class PeerData; + struct PollAnswer { TextWithEntities text; QByteArray option; std::optional media; + std::vector> recentVoters; int votes = 0; bool chosen = false; bool correct = false; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 189e2eb22a..9bd0446074 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4916,6 +4916,7 @@ void Session::sendWebPageGamePollTodoListNotifications() { } } for (const auto &poll : base::take(_pollsUpdated)) { + _pollUpdates.fire_copy(poll); if (const auto i = _pollViews.find(poll); i != _pollViews.end()) { resize.insert(end(resize), begin(i->second), end(i->second)); } @@ -4935,6 +4936,10 @@ rpl::producer> Session::webPageUpdates() const { return _webpageUpdates.events(); } +rpl::producer> Session::pollUpdates() const { + return _pollUpdates.events(); +} + void Session::channelDifferenceTooLong(not_null channel) { _channelDifferenceTooLong.fire_copy(channel); } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index e30e903568..779ea34e9e 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -849,6 +849,7 @@ public: [[nodiscard]] bool hasPendingWebPageGamePollTodoListNotification() const; void sendWebPageGamePollTodoListNotifications(); [[nodiscard]] rpl::producer> webPageUpdates() const; + [[nodiscard]] rpl::producer> pollUpdates() const; void channelDifferenceTooLong(not_null channel); [[nodiscard]] rpl::producer> channelDifferenceTooLong() const; @@ -1243,6 +1244,7 @@ private: base::flat_set> _todoListsUpdated; rpl::event_stream> _webpageUpdates; + rpl::event_stream> _pollUpdates; rpl::event_stream> _channelDifferenceTooLong; rpl::event_stream> _documentLoadProgress; base::flat_set> _suggestToGigagroup; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 1184b9b322..b7963ee346 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -127,6 +127,15 @@ mediaviewVoteButton: RoundButton(defaultActiveButton) { } mediaviewVoteButtonMargin: 8px; +mediaviewPollVotersUserpics: GroupCallUserpics { + size: 24px; + shift: 8px; + stroke: 2px; + align: align(left); +} +mediaviewPollVotersPadding: margins(12px, 0px, 14px, 0px); +mediaviewPollVotersSpacing: 6px; + mediaviewFileRed: icon { { size(25px, 25px), mediaviewFileBg }, { "mediaview/file_corner", mediaviewFileRedCornerFg }, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 6913fa8afa..9850f53ab5 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -63,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item_helpers.h" #include "history/view/media/history_view_media.h" +#include "history/view/history_view_group_call_bar.h" #include "history/view/reactions/history_view_reactions_selector.h" #include "data/components/sponsored_messages.h" #include "data/data_group_call.h" @@ -98,6 +99,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_media_view.h" #include "styles/style_calls.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" #include "platform/platform_text_recognition.h" @@ -1633,9 +1635,29 @@ void OverlayWidget::resizeCenteredControls() { refreshClipControllerGeometry(); refreshSponsoredButtonGeometry(); refreshVoteButton(); + refreshPollVotersWidget(); refreshVoteButtonGeometry(); + refreshPollVotersWidgetGeometry(); refreshCaptionGeometry(); refreshSponsoredButtonWidth(); + + _pollUpdateLifetime.destroy(); + if (currentPollAnswer()) { + const auto media = _message ? _message->media() : nullptr; + const auto poll = media ? media->poll() : nullptr; + if (poll && _session) { + _session->data().pollUpdates( + ) | rpl::filter([=](not_null updated) { + return updated == poll; + }) | rpl::on_next([=] { + refreshVoteButton(); + refreshPollVotersWidget(); + refreshVoteButtonGeometry(); + refreshPollVotersWidgetGeometry(); + refreshCaptionGeometry(); + }, _pollUpdateLifetime); + } + } } void OverlayWidget::refreshCaptionGeometry() { @@ -1664,6 +1686,8 @@ void OverlayWidget::refreshCaptionGeometry() { ? (_sponsoredButton->y() - st::mediaviewCaptionMargin.height()) : _voteButton ? (_voteButton->y() - st::mediaviewCaptionMargin.height()) + : _pollVotersWidget + ? (_pollVotersWidget->y() - st::mediaviewCaptionMargin.height()) : (_streamed && _streamed->controls) ? (_streamed->controls->y() - st::mediaviewCaptionMargin.height()) : _groupThumbs @@ -1789,18 +1813,23 @@ void OverlayWidget::refreshVoteButton() { tr::lng_polls_submit_votes(), st::mediaviewVoteButton); _voteButton->setTextTransform(TextTransform::NoTransform); - const auto effect = Ui::CreateChild>( + const auto effect = Ui::CreateChild( _voteButton.data()); effect->setOpacity(_controlsOpacity.current()); _voteButton->setGraphicsEffect(effect); - const auto itemId = _message->fullId(); - const auto option = answer->option; _voteButton->setClickedCallback([=] { - if (_session) { - _session->api().polls().sendVotes(itemId, { option }); - _voteButton.destroy(); - refreshCaptionGeometry(); + if (!_session) { + return; } + const auto current = currentPollAnswer(); + if (!current) { + return; + } + _session->api().polls().sendVotes( + _message->fullId(), + { current->option }); + _voteButton.destroy(); + refreshCaptionGeometry(); }); } _voteButton->show(); @@ -1824,6 +1853,97 @@ void OverlayWidget::refreshVoteButtonGeometry() { - st::mediaviewVoteButtonMargin); } +void OverlayWidget::refreshPollVotersWidget() { + const auto answer = currentPollAnswer(); + const auto show = answer + && (answer->votes > 0) + && !_voteButton; + if (!show) { + _pollVotersWidget.destroy(); + return; + } + + _pollVotersWidget.destroy(); + _pollVotersWidget.create(_body); + + const auto effect = Ui::CreateChild( + _pollVotersWidget.data()); + effect->setOpacity(_controlsOpacity.current()); + _pollVotersWidget->setGraphicsEffect(effect); + + auto prepared = HistoryView::PrepareUserpicsInRow( + answer->recentVoters, + st::mediaviewPollVotersUserpics, + 3); + + const auto votesText = tr::lng_polls_votes_count( + tr::now, + lt_count, + answer->votes); + const auto &font = st::semiboldFont; + const auto textWidth = font->width(votesText); + + const auto userpicsWidth = prepared.width; + const auto spacing = (userpicsWidth > 0) + ? st::mediaviewPollVotersSpacing + : 0; + const auto &padding = st::mediaviewPollVotersPadding; + const auto widgetWidth = padding.left() + + userpicsWidth + + spacing + + textWidth + + padding.right(); + const auto widgetHeight = st::mediaviewVoteButton.height; + + _pollVotersWidget->resize(widgetWidth, widgetHeight); + + const auto userpicSize = st::mediaviewPollVotersUserpics.size; + const auto raw = _pollVotersWidget.data(); + raw->paintRequest() | rpl::on_next([=, + image = std::move(prepared.image)] { + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + + const auto radius = st::mediaviewCaptionRadius; + p.setPen(Qt::NoPen); + p.setBrush(st::mediaviewCaptionBg); + p.drawRoundedRect(raw->rect(), radius, radius); + + auto left = padding.left(); + + if (!image.isNull()) { + const auto top = (widgetHeight - userpicSize) / 2; + p.drawImage(left, top, image); + left += userpicsWidth + spacing; + } + + p.setFont(font); + p.setPen(st::mediaviewCaptionFg); + const auto textTop = (widgetHeight + font->ascent - font->descent) / 2; + p.drawText(left, textTop, votesText); + }, raw->lifetime()); + + _pollVotersWidget->show(); +} + +void OverlayWidget::refreshPollVotersWidgetGeometry() { + if (!_pollVotersWidget) { + return; + } + const auto controllerBottom = (_groupThumbs && !_fullScreenVideo) + ? _groupThumbsTop + : height(); + _pollVotersWidget->moveToLeft( + (width() - _pollVotersWidget->width()) / 2, + controllerBottom + - ((_streamed && _streamed->controls) + ? (_streamed->controls->height() + + st::mediaviewCaptionPadding.bottom()) + : 0) + - _pollVotersWidget->height() + - st::mediaviewVoteButtonMargin); +} + void OverlayWidget::fillContextMenuActions( const Ui::Menu::MenuCallback &addAction) { if (_message && _message->isSponsored()) { @@ -2158,6 +2278,15 @@ bool OverlayWidget::updateControlsAnimation(crl::time now) { Qt::WA_TransparentForMouseEvents, value < 1); } + if (_pollVotersWidget) { + const auto value = _controlsOpacity.current(); + if (const auto e = _pollVotersWidget->graphicsEffect()) { + static_cast(e)->setOpacity(value); + } + _pollVotersWidget->setAttribute( + Qt::WA_TransparentForMouseEvents, + value < 1); + } _helper->setControlsOpacity(_controlsOpacity.current()); const auto content = finalContentRect(); const auto siblingType = (_over == Over::LeftStories) @@ -7255,6 +7384,8 @@ void OverlayWidget::clearBeforeHide() { _groupThumbsRect = QRect(); _sponsoredButton = nullptr; _voteButton.destroy(); + _pollVotersWidget.destroy(); + _pollUpdateLifetime.destroy(); _showRecognitionResults = false; } @@ -7270,6 +7401,7 @@ void OverlayWidget::clearAfterHide() { _staticContent = QImage(); _themePreview = nullptr; _voteButton.destroyDelayed(); + _pollVotersWidget.destroyDelayed(); _themeApply.destroyDelayed(); _themeCancel.destroyDelayed(); _themeShare.destroyDelayed(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 468514549a..0c99e2d5cb 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -440,6 +440,8 @@ private: void refreshVoteButton(); void refreshVoteButtonGeometry(); + void refreshPollVotersWidget(); + void refreshPollVotersWidgetGeometry(); [[nodiscard]] const PollAnswer *currentPollAnswer() const; void documentUpdated(not_null document); @@ -747,6 +749,8 @@ private: base::unique_qptr _sponsoredButton; object_ptr _voteButton = { nullptr }; + object_ptr _pollVotersWidget = { nullptr }; + rpl::lifetime _pollUpdateLifetime; bool _receiveMouse = true; bool _processingKeyPress = false;