diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 0d78200d99..e81de07fc3 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2797,7 +2797,7 @@ void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &msgs ++i; } } - _session->data().sendWebPageGameNotifications(); + _session->data().sendWebPageGamePollNotifications(); } void ApiWrap::stickersSaveOrder() { diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 53782fc0c4..a407b34ba5 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -152,6 +152,10 @@ LocationData *Media::location() const { return nullptr; } +PollData *Media::poll() const { + return nullptr; +} + bool Media::uploading() const { return false; } @@ -1178,4 +1182,52 @@ std::unique_ptr MediaInvoice::createView( return std::make_unique(message, &_invoice); } +MediaPoll::MediaPoll( + not_null parent, + not_null poll) +: Media(parent) +, _poll(poll) { +} + +MediaPoll::~MediaPoll() { +} + +std::unique_ptr MediaPoll::clone(not_null parent) { + return std::make_unique(parent, _poll); +} + +PollData *MediaPoll::poll() const { + return _poll; +} + +QString MediaPoll::chatsListText() const { + return QString(); // #TODO polls +} + +QString MediaPoll::notificationText() const { + return QString(); // #TODO polls +} + +QString MediaPoll::pinnedTextSubstring() const { + return QString(); // #TODO polls +} + +TextWithEntities MediaPoll::clipboardText() const { + return TextWithEntities(); // #TODO polls +} + +bool MediaPoll::updateInlineResultMedia(const MTPMessageMedia &media) { + return false; +} + +bool MediaPoll::updateSentMedia(const MTPMessageMedia &media) { + return false; +} + +std::unique_ptr MediaPoll::createView( + not_null message, + not_null realParent) { + return nullptr; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 3bd027908b..c6b1e29289 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -41,7 +41,6 @@ struct SharedContact { QString firstName; QString lastName; QString phoneNumber; - }; struct Call { @@ -49,7 +48,6 @@ struct Call { int duration = 0; FinishReason finishReason = FinishReason::Missed; - }; struct Invoice { @@ -60,7 +58,6 @@ struct Invoice { QString description; PhotoData *photo = nullptr; bool isTest = false; - }; class Media { @@ -80,6 +77,7 @@ public: virtual GameData *game() const; virtual const Invoice *invoice() const; virtual LocationData *location() const; + virtual PollData *poll() const; virtual bool uploading() const; virtual Storage::SharedMediaTypesMask sharedMediaTypes() const; @@ -381,6 +379,33 @@ private: }; +class MediaPoll : public Media { +public: + MediaPoll( + not_null parent, + not_null poll); + ~MediaPoll(); + + std::unique_ptr clone(not_null parent) override; + + PollData *poll() const override; + + QString chatsListText() const override; + QString notificationText() const override; + QString pinnedTextSubstring() const override; + TextWithEntities clipboardText() const override; + + bool updateInlineResultMedia(const MTPMessageMedia &media) override; + bool updateSentMedia(const MTPMessageMedia &media) override; + std::unique_ptr createView( + not_null message, + not_null realParent) override; + +private: + not_null _poll; + +}; + TextWithEntities WithCaptionClipboardText( const QString &attachType, TextWithEntities &&caption); diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp new file mode 100644 index 0000000000..eccf2df929 --- /dev/null +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -0,0 +1,122 @@ +/* +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 "data/data_poll.h" + +namespace { + +const PollAnswer *AnswerByOption( + const std::vector &list, + const QByteArray &option) { + const auto i = ranges::find( + list, + option, + [](const PollAnswer &a) { return a.option; }); + return (i != end(list)) ? &*i : nullptr; +} + +PollAnswer *AnswerByOption( + std::vector &list, + const QByteArray &option) { + return const_cast(AnswerByOption( + std::as_const(list), + option)); +} + +} // namespace + +PollData::PollData(PollId id) : id(id) { +} + +bool PollData::applyChanges(const MTPDpoll &poll) { + Expects(poll.vid.v == id); + + const auto newQuestion = qs(poll.vquestion); + const auto newClosed = poll.is_closed(); + auto newAnswers = ranges::view::all( + poll.vanswers.v + ) | ranges::view::transform([](const MTPPollAnswer &data) { + return data.match([](const MTPDpollAnswer &answer) { + auto result = PollAnswer(); + result.option = answer.voption.v; + result.text = qs(answer.vtext); + return result; + }); + }) | ranges::to_vector; + + const auto changed1 = (question != newQuestion) + || (closed != newClosed); + const auto changed2 = (answers != newAnswers); + if (!changed1 && !changed2) { + return false; + } + if (changed1) { + question = newQuestion; + closed = newClosed; + } + if (changed2) { + std::swap(answers, newAnswers); + for (const auto &old : newAnswers) { + if (const auto current = answerByOption(old.option)) { + current->votes = old.votes; + current->chosen = old.chosen; + } + } + } + return true; +} + +bool PollData::applyResults(const MTPPollResults &results) { + return results.match([&](const MTPDpollResults &results) { + const auto newTotalVoters = results.has_total_voters() + ? results.vtotal_voters.v + : totalVoters; + auto changed = (newTotalVoters != totalVoters); + if (results.has_results()) { + for (const auto &result : results.vresults.v) { + if (applyResultToAnswers(result, results.is_min())) { + changed = true; + } + } + } + totalVoters = newTotalVoters; + return changed; + }); +} + +PollAnswer *PollData::answerByOption(const QByteArray &option) { + return AnswerByOption(answers, option); +} + +const PollAnswer *PollData::answerByOption(const QByteArray &option) const { + return AnswerByOption(answers, option); +} + +bool PollData::applyResultToAnswers( + const MTPPollAnswerVoters &result, + bool isMinResults) { + return result.match([&](const MTPDpollAnswerVoters &voters) { + const auto &option = voters.voption.v; + const auto answer = answerByOption(option); + if (!answer) { + return false; + } + auto changed = (answer->votes != voters.vvoters.v); + if (changed) { + answer->votes = voters.vvoters.v; + } + if (!isMinResults) { + if (answer->chosen != voters.is_chosen()) { + answer->chosen = voters.is_chosen(); + changed = true; + } + } else if (const auto existing = answerByOption(option)) { + answer->chosen = existing->chosen; + } + return changed; + }); +} diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h new file mode 100644 index 0000000000..f197199c88 --- /dev/null +++ b/Telegram/SourceFiles/data/data_poll.h @@ -0,0 +1,46 @@ +/* +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 +*/ +#pragma once + +struct PollAnswer { + QString text; + QByteArray option; + int votes = 0; + bool chosen = false; +}; + +inline bool operator==(const PollAnswer &a, const PollAnswer &b) { + return (a.text == b.text) + && (a.option == b.option); +} + +inline bool operator!=(const PollAnswer &a, const PollAnswer &b) { + return !(a == b); +} + +struct PollData { + explicit PollData(PollId id); + + bool applyChanges(const MTPDpoll &poll); + bool applyResults(const MTPPollResults &results); + + PollAnswer *answerByOption(const QByteArray &option); + const PollAnswer *answerByOption(const QByteArray &option) const; + + PollId id = 0; + QString question; + std::vector answers; + int totalVoters = 0; + bool closed = false; + +private: + bool applyResultToAnswers( + const MTPPollAnswerVoters &result, + bool isMinResults); + +}; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 700efedab9..20a2baaf96 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_web_page.h" #include "data/data_game.h" +#include "data/data_poll.h" namespace Data { namespace { @@ -297,6 +298,14 @@ void Session::requestDocumentViewRepaint( } } +void Session::requestPollViewRepaint(not_null poll) { + if (const auto i = _pollViews.find(poll); i != _pollViews.end()) { + for (const auto view : i->second) { + requestViewResize(view); + } + } +} + void Session::markMediaRead(not_null document) { const auto i = _documentItems.find(document); if (i != end(_documentItems)) { @@ -1490,6 +1499,55 @@ void Session::gameApplyFields( notifyGameUpdateDelayed(game); } +not_null Session::poll(PollId id) { + auto i = _polls.find(id); + if (i == _polls.cend()) { + i = _polls.emplace(id, std::make_unique(id)).first; + } + return i->second.get(); +} + +not_null Session::poll(const MTPPoll &data) { + return data.match([&](const MTPDpoll &data) { + const auto id = data.vid.v; + const auto result = poll(id); + const auto changed = result->applyChanges(data); + if (changed) { + notifyPollUpdateDelayed(result); + } + return result; + }); +} + +not_null Session::poll(const MTPDmessageMediaPoll &data) { + const auto result = poll(data.vpoll); + const auto changed = result->applyResults(data.vresults); + if (changed) { + requestPollViewRepaint(result); + } + return result; +} + +void Session::applyPollUpdate(const MTPDupdateMessagePoll &update) { + const auto poll = [&] { + if (update.has_poll()) { + return update.vpoll.match([&](const MTPDpoll &data) { + const auto i = _polls.find(data.vid.v); + return (i != end(_polls)) ? i->second.get() : nullptr; + }); + } + const auto item = App::histItemById( + peerToChannel(peerFromMTP(update.vpeer)), + update.vmsg_id.v); + return (item && item->media()) + ? item->media()->poll() + : nullptr; + }(); + if (poll && poll->applyResults(update.vresults)) { + requestPollViewRepaint(poll); + } +} + not_null Session::location(const LocationCoords &coords) { auto i = _locations.find(coords); if (i == _locations.cend()) { @@ -1590,6 +1648,24 @@ void Session::unregisterGameView( } } +void Session::registerPollView( + not_null poll, + not_null view) { + _pollViews[poll].insert(view); +} + +void Session::unregisterPollView( + not_null poll, + not_null view) { + const auto i = _pollViews.find(poll); + if (i != _pollViews.end()) { + auto &items = i->second; + if (items.remove(view) && items.empty()) { + _pollViews.erase(i); + } + } +} + void Session::registerContactView( UserId contactId, not_null view) { @@ -1714,23 +1790,37 @@ QString Session::findContactPhone(UserId contactId) const { return QString(); } +bool Session::hasPendingWebPageGamePollNotification() const { + return !_webpagesUpdated.empty() + || !_gamesUpdated.empty() + || !_pollsUpdated.empty(); +} + void Session::notifyWebPageUpdateDelayed(not_null page) { - const auto invoke = _webpagesUpdated.empty() && _gamesUpdated.empty(); + const auto invoke = !hasPendingWebPageGamePollNotification(); _webpagesUpdated.insert(page); if (invoke) { - crl::on_main(_session, [=] { sendWebPageGameNotifications(); }); + crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); }); } } void Session::notifyGameUpdateDelayed(not_null game) { - const auto invoke = _webpagesUpdated.empty() && _gamesUpdated.empty(); + const auto invoke = !hasPendingWebPageGamePollNotification(); _gamesUpdated.insert(game); if (invoke) { - crl::on_main(_session, [=] { sendWebPageGameNotifications(); }); + crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); }); } } -void Session::sendWebPageGameNotifications() { +void Session::notifyPollUpdateDelayed(not_null poll) { + const auto invoke = !hasPendingWebPageGamePollNotification(); + _pollsUpdated.insert(poll); + if (invoke) { + crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); }); + } +} + +void Session::sendWebPageGamePollNotifications() { for (const auto page : base::take(_webpagesUpdated)) { const auto i = _webpageViews.find(page); if (i != _webpageViews.end()) { @@ -1746,6 +1836,13 @@ void Session::sendWebPageGameNotifications() { } } } + for (const auto poll : base::take(_pollsUpdated)) { + if (const auto i = _pollViews.find(poll); i != _pollViews.end()) { + for (const auto view : i->second) { + requestViewResize(view); + } + } + } } void Session::registerItemView(not_null view) { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 83f71bf31e..44076a24ff 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -246,6 +246,7 @@ public: not_null document); void requestDocumentViewRepaint(not_null document); void markMediaRead(not_null document); + void requestPollViewRepaint(not_null poll); not_null photo(PhotoId id); not_null photo(const MTPPhoto &data); @@ -330,6 +331,11 @@ public: not_null original, const MTPGame &data); + not_null poll(PollId id); + not_null poll(const MTPPoll &data); + not_null poll(const MTPDmessageMediaPoll &data); + void applyPollUpdate(const MTPDupdateMessagePoll &update); + not_null location(const LocationCoords &coords); void registerPhotoItem( @@ -362,6 +368,12 @@ public: void unregisterGameView( not_null game, not_null view); + void registerPollView( + not_null poll, + not_null view); + void unregisterPollView( + not_null poll, + not_null view); void registerContactView( UserId contactId, not_null view); @@ -386,7 +398,9 @@ public: void notifyWebPageUpdateDelayed(not_null page); void notifyGameUpdateDelayed(not_null game); - void sendWebPageGameNotifications(); + void notifyPollUpdateDelayed(not_null poll); + bool hasPendingWebPageGamePollNotification() const; + void sendWebPageGamePollNotifications(); void stopAutoplayAnimations(); @@ -604,21 +618,27 @@ private: std::unordered_map< WebPageId, std::unique_ptr> _webpages; - std::unordered_map< - LocationCoords, - std::unique_ptr> _locations; std::map< not_null, base::flat_set>> _webpageItems; std::map< not_null, base::flat_set>> _webpageViews; + std::unordered_map< + LocationCoords, + std::unique_ptr> _locations; + std::unordered_map< + PollId, + std::unique_ptr> _polls; std::unordered_map< GameId, std::unique_ptr> _games; std::map< not_null, base::flat_set>> _gameViews; + std::map< + not_null, + base::flat_set>> _pollViews; std::map< UserId, base::flat_set>> _contactItems; @@ -631,6 +651,7 @@ private: base::flat_set> _webpagesUpdated; base::flat_set> _gamesUpdated; + base::flat_set> _pollsUpdated; std::deque _pinnedDialogs; base::flat_map> _feeds; diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 8c7b2ae52d..5a0b89d289 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -243,6 +243,7 @@ class DocumentData; class PhotoData; struct WebPageData; struct GameData; +struct PollData; class AudioMsgId; class PhotoClickHandler; @@ -262,6 +263,7 @@ using AudioId = uint64; using DocumentId = uint64; using WebPageId = uint64; using GameId = uint64; +using PollId = uint64; constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL); using PreparedPhotoThumbs = QMap; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index fb740193ce..b90954c178 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -138,8 +138,8 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { }); }, [](const MTPDmessageMediaInvoice &) { return Result::Good; - }, [](const MTPDmessageMediaPoll &) { // #TODO polls - return Result::Unsupported; + }, [](const MTPDmessageMediaPoll &) { + return Result::Good; }, [](const MTPDmessageMediaUnsupported &) { return Result::Unsupported; }); diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index 2cf34a94b4..484eedfce7 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -855,8 +855,10 @@ std::unique_ptr HistoryMessage::CreateMedia( }); }, [&](const MTPDmessageMediaInvoice &media) -> Result { return std::make_unique(item, media); - }, [&](const MTPDmessageMediaPoll &media) -> Result { // #TODO polls - return nullptr; + }, [&](const MTPDmessageMediaPoll &media) -> Result { + return std::make_unique( + item, + Auth().data().poll(media)); }, [](const MTPDmessageMediaEmpty &) -> Result { return nullptr; }, [](const MTPDmessageMediaUnsupported &) -> Result { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index bc36da0c18..6e295c62ba 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -5928,7 +5928,7 @@ void HistoryWidget::gotPreview(QString links, const MTPMessageMedia &result, mtp : nullptr; updatePreview(); } - Auth().data().sendWebPageGameNotifications(); + Auth().data().sendWebPageGamePollNotifications(); } else if (result.type() == mtpc_messageMediaEmpty) { _previewCache.insert(links, 0); if (links == _previewLinks && !_previewCancelled) { diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index ce0fba1216..6f8a2c29b2 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -4246,7 +4246,7 @@ void MainWidget::feedUpdate(const MTPUpdate &update) { // Update web page anyway. Auth().data().webpage(d.vwebpage); _history->updatePreview(); - Auth().data().sendWebPageGameNotifications(); + Auth().data().sendWebPageGamePollNotifications(); ptsUpdateAndApply(d.vpts.v, d.vpts_count.v, update); } break; @@ -4257,7 +4257,7 @@ void MainWidget::feedUpdate(const MTPUpdate &update) { // Update web page anyway. Auth().data().webpage(d.vwebpage); _history->updatePreview(); - Auth().data().sendWebPageGameNotifications(); + Auth().data().sendWebPageGamePollNotifications(); auto channel = App::channelLoaded(d.vchannel_id.v); if (channel && !_handlingChannelDifference) { @@ -4271,6 +4271,10 @@ void MainWidget::feedUpdate(const MTPUpdate &update) { } } break; + case mtpc_updateMessagePoll: { + Auth().data().applyPollUpdate(update.c_updateMessagePoll()); + } break; + case mtpc_updateUserTyping: { auto &d = update.c_updateUserTyping(); const auto userId = peerFromUser(d.vuser_id); diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt index 03c8a1335d..e09acab865 100644 --- a/Telegram/gyp/telegram_sources.txt +++ b/Telegram/gyp/telegram_sources.txt @@ -166,6 +166,8 @@ <(src_loc)/data/data_peer_values.h <(src_loc)/data/data_photo.cpp <(src_loc)/data/data_photo.h +<(src_loc)/data/data_poll.cpp +<(src_loc)/data/data_poll.h <(src_loc)/data/data_search_controller.cpp <(src_loc)/data/data_search_controller.h <(src_loc)/data/data_session.cpp