diff --git a/Telegram/SourceFiles/data/components/factchecks.cpp b/Telegram/SourceFiles/data/components/factchecks.cpp new file mode 100644 index 000000000..4e5fcd090 --- /dev/null +++ b/Telegram/SourceFiles/data/components/factchecks.cpp @@ -0,0 +1,136 @@ +/* +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/components/factchecks.h" + +#include "apiwrap.h" +#include "base/random.h" +#include "data/data_session.h" +#include "history/view/media/history_view_web_page.h" +#include "history/view/history_view_message.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" + +namespace Data { +namespace { + +constexpr auto kRequestDelay = crl::time(1000); + +} // namespace + +Factchecks::Factchecks(not_null session) +: _session(session) +, _requestTimer([=] { request(); }) { +} + +void Factchecks::requestFor(not_null item) { + subscribeIfNotYet(); + + if (const auto factcheck = item->Get()) { + factcheck->requested = true; + } + if (!_requestTimer.isActive()) { + _requestTimer.callOnce(kRequestDelay); + } + const auto changed = !_pending.empty() + && (_pending.front()->history() != item->history()); + const auto added = _pending.emplace(item).second; + if (changed) { + request(); + } else if (added && _pending.size() == 1) { + _requestTimer.callOnce(kRequestDelay); + } +} + +void Factchecks::subscribeIfNotYet() { + if (_subscribed) { + return; + } + _subscribed = true; + + _session->data().itemRemoved( + ) | rpl::start_with_next([=](not_null item) { + _pending.remove(item); + const auto i = ranges::find(_requested, item.get()); + if (i != end(_requested)) { + *i = nullptr; + } + }, _lifetime); +} + +void Factchecks::request() { + _requestTimer.cancel(); + + if (!_requested.empty() || _pending.empty()) { + return; + } + _session->api().request(base::take(_requestId)).cancel(); + + auto ids = QVector(); + ids.reserve(_pending.size()); + const auto history = _pending.front()->history(); + for (auto i = begin(_pending); i != end(_pending);) { + const auto &item = *i; + if (item->history() == history) { + _requested.push_back(item); + ids.push_back(MTP_int(item->id.bare)); + i = _pending.erase(i); + } else { + ++i; + } + } + _requestId = _session->api().request(MTPmessages_GetFactCheck( + history->peer->input, + MTP_vector(std::move(ids)) + )).done([=](const MTPVector &result) { + _requestId = 0; + const auto &list = result.v; + auto index = 0; + for (const auto &item : base::take(_requested)) { + if (!item) { + } else if (index >= list.size()) { + item->setFactcheck({}); + } else { + item->setFactcheck(FromMTP(item, &list[index])); + } + ++index; + } + if (!_pending.empty()) { + request(); + } + }).fail([=] { + _requestId = 0; + for (const auto &item : base::take(_requested)) { + if (item) { + item->setFactcheck({}); + } + } + if (!_pending.empty()) { + request(); + } + }).send(); +} + +std::unique_ptr Factchecks::makeMedia( + not_null view, + not_null factcheck) { + if (!factcheck->page) { + factcheck->page = view->history()->owner().webpage( + base::RandomValue(), + tr::lng_factcheck_title(tr::now), + factcheck->data.text); + } + return std::make_unique( + view, + factcheck->page, + MediaWebPageFlags()); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/factchecks.h b/Telegram/SourceFiles/data/components/factchecks.h new file mode 100644 index 000000000..452706f9b --- /dev/null +++ b/Telegram/SourceFiles/data/components/factchecks.h @@ -0,0 +1,51 @@ +/* +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 + +#include "base/timer.h" + +class HistoryItem; +struct HistoryMessageFactcheck; + +namespace HistoryView { +class Message; +class WebPage; +} // namespace HistoryView + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class Factchecks final { +public: + explicit Factchecks(not_null session); + + void requestFor(not_null item); + [[nodiscard]] std::unique_ptr makeMedia( + not_null view, + not_null factcheck); + +private: + void subscribeIfNotYet(); + void request(); + + const not_null _session; + + base::Timer _requestTimer; + base::flat_set> _pending; + std::vector _requested; + mtpRequestId _requestId = 0; + bool _subscribed = false; + + rpl::lifetime _lifetime; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index d101d5d88..529b933d4 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4250,29 +4250,27 @@ void Session::notifyPollUpdateDelayed(not_null poll) { } void Session::sendWebPageGamePollNotifications() { + auto resize = std::vector>(); for (const auto &page : base::take(_webpagesUpdated)) { _webpageUpdates.fire_copy(page); - const auto i = _webpageViews.find(page); - if (i != _webpageViews.end()) { - for (const auto &view : i->second) { - requestViewResize(view); - } + if (const auto i = _webpageViews.find(page) + ; i != _webpageViews.end()) { + resize.insert(end(resize), begin(i->second), end(i->second)); } } for (const auto &game : base::take(_gamesUpdated)) { if (const auto i = _gameViews.find(game); i != _gameViews.end()) { - for (const auto &view : i->second) { - requestViewResize(view); - } + resize.insert(end(resize), begin(i->second), end(i->second)); } } 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); - } + resize.insert(end(resize), begin(i->second), end(i->second)); } } + for (const auto &view : resize) { + requestViewResize(view); + } } rpl::producer> Session::webPageUpdates() const { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index 61cf9a889..8fa440bf6 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -1306,6 +1306,8 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { && !link && (view->hasVisibleText() || mediaHasTextForCopy + || (item->Has() + && !item->Get()->data.text.empty()) || item->Has())) { _menu->addAction(tr::lng_context_copy_text(tr::now), [=] { copyContextText(itemId); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 7cb74b189..3a2ed484a 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -415,6 +415,11 @@ HistoryItem::HistoryItem( } setReactions(data.vreactions()); applyTTL(data); + + if (const auto check = FromMTP(this, data.vfactcheck())) { + AddComponents(HistoryMessageFactcheck::Bit()); + Get()->data = check; + } } } @@ -1494,6 +1499,33 @@ void HistoryItem::addLogEntryOriginal( content); } +void HistoryItem::setFactcheck(MessageFactcheck info) { + if (!info) { + if (Has()) { + RemoveComponents(HistoryMessageFactcheck::Bit()); + history()->owner().requestItemResize(this); + } + } else { + AddComponents(HistoryMessageFactcheck::Bit()); + const auto factcheck = Get(); + if (factcheck->data.hash == info.hash + && (info.needCheck || !factcheck->data.needCheck)) { + return; + } else if (factcheck->data.text != info.text + || factcheck->data.country != info.country + || factcheck->data.hash != info.hash) { + factcheck->data = std::move(info); + factcheck->requested = false; + history()->owner().requestItemResize(this); + } + } +} + +bool HistoryItem::hasUnrequestedFactcheck() const { + const auto factcheck = Get(); + return factcheck && factcheck->data.needCheck && !factcheck->requested; +} + PeerData *HistoryItem::specialNotificationPeer() const { return (mentionsMe() && !_history->peer->isUser()) ? from().get() @@ -3143,6 +3175,8 @@ EffectId HistoryItem::effectId() const { bool HistoryItem::isEmpty() const { return _text.empty() && !_media + && (!Has() + || Get()->data.text.empty()) && !Has(); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 1b006e35a..5a864d076 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -23,9 +23,11 @@ struct HistoryMessageReplyMarkup; struct HistoryMessageTranslation; struct HistoryMessageForwarded; struct HistoryMessageSavedMediaData; +struct HistoryMessageFactcheck; struct HistoryServiceDependentData; enum class HistorySelfDestructType; struct PreparedServiceText; +struct MessageFactcheck; class ReplyKeyboard; struct LanguageId; @@ -204,6 +206,8 @@ public: WebPageId localId, const QString &label, const TextWithEntities &content); + void setFactcheck(MessageFactcheck info); + [[nodiscard]] bool hasUnrequestedFactcheck() const; [[nodiscard]] not_null notificationThread() const; [[nodiscard]] not_null history() const { diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index dc4a38c7a..54e42a16e 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -1062,6 +1062,31 @@ HistoryMessageLogEntryOriginal &HistoryMessageLogEntryOriginal::operator=( HistoryMessageLogEntryOriginal::~HistoryMessageLogEntryOriginal() = default; +MessageFactcheck FromMTP( + not_null item, + const tl::conditional &factcheck) { + auto result = MessageFactcheck(); + if (!factcheck) { + return result; + } + const auto &data = factcheck->data(); + if (const auto text = data.vtext()) { + const auto &data = text->data(); + result.text = { + qs(data.vtext()), + Api::EntitiesFromMTP( + &item->history()->session(), + data.ventities().v), + }; + } + if (const auto country = data.vcountry()) { + result.country = qs(country->v); + } + result.hash = data.vhash().v; + result.needCheck = data.is_need_check(); + return result; +} + HistoryDocumentCaptioned::HistoryDocumentCaptioned() : caption(st::msgFileMinWidth - st::msgPadding.left() - st::msgPadding.right()) { } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 44f31f3cd..fa7ffbf3a 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -562,6 +562,31 @@ struct HistoryMessageLogEntryOriginal }; +struct MessageFactcheck { + TextWithEntities text; + QString country; + uint64 hash = 0; + bool needCheck = false; + + [[nodiscard]] bool empty() const { + return text.empty() && country.isEmpty() && !hash; + } + explicit operator bool() const { + return !empty(); + } +}; + +[[nodiscard]] MessageFactcheck FromMTP( + not_null item, + const tl::conditional &factcheck); + +struct HistoryMessageFactcheck +: public RuntimeComponent { + MessageFactcheck data; + WebPageData *page = nullptr; + bool requested = false; +}; + struct HistoryServiceData : public RuntimeComponent { std::vector textLinks; diff --git a/Telegram/SourceFiles/history/history_item_text.cpp b/Telegram/SourceFiles/history/history_item_text.cpp index 1c66c59a4..ba35168cf 100644 --- a/Telegram/SourceFiles/history/history_item_text.cpp +++ b/Telegram/SourceFiles/history/history_item_text.cpp @@ -46,6 +46,12 @@ TextForMimeData HistoryItemText(not_null item) { titleResult.append('\n').append(std::move(descriptionResult)); return titleResult; }(); + auto factcheckResult = [&] { + const auto factcheck = item->Get(); + return factcheck + ? TextForMimeData::Rich(base::duplicate(factcheck->data.text)) + : TextForMimeData(); + }(); auto result = textResult; if (result.empty()) { result = std::move(mediaResult); @@ -57,6 +63,11 @@ TextForMimeData HistoryItemText(not_null item) { } else if (!logEntryOriginalResult.empty()) { result.append(u"\n\n"_q).append(std::move(logEntryOriginalResult)); } + if (result.empty()) { + result = std::move(factcheckResult); + } else if (!factcheckResult.empty()) { + result.append(u"\n\n"_q).append(std::move(factcheckResult)); + } return result; } diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index cf19c8f9a..861c96ee7 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/round_rect.h" #include "ui/text/text_utilities.h" #include "ui/power_saving.h" +#include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" #include "data/data_session.h" #include "data/data_user.h" @@ -799,6 +800,24 @@ QSize Message::performCountOptimalSize() { RemoveComponents(Reply::Bit()); } + const auto factcheck = item->Get(); + if (factcheck && !factcheck->data.text.empty()) { + AddComponents(Factcheck::Bit()); + Get()->page = history()->session().factchecks().makeMedia( + this, + factcheck); + + auto copy = data()->originalText(); + if (!copy.text.contains("FACT CHECK")) { + copy.append("\n\nFACT CHECK!!\n\n").append(factcheck->data.text); + crl::on_main(this, [=] { + data()->setText(std::move(copy)); + }); + } + } else { + RemoveComponents(Factcheck::Bit()); + } + const auto markup = item->inlineReplyMarkup(); const auto reactionsKey = [&] { return embedReactionsInBottomInfo() @@ -1069,6 +1088,10 @@ void Message::draw(Painter &p, const PaintContext &context) const { const auto item = data(); const auto media = this->media(); + if (item->hasUnrequestedFactcheck()) { + item->history()->session().factchecks().requestFor(item); + } + const auto stm = context.messageStyle(); const auto bubble = drawBubble(); diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index a8c172c03..5f64193d7 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -44,6 +44,11 @@ struct LogEntryOriginal std::unique_ptr page; }; +struct Factcheck +: public RuntimeComponent { + std::unique_ptr page; +}; + struct PsaTooltipState : public RuntimeComponent { QString type; mutable ClickHandlerPtr link; diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index fc8435d66..951ec735a 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/file_upload.h" #include "storage/storage_account.h" #include "storage/storage_facade.h" +#include "data/components/factchecks.h" #include "data/components/recent_peers.h" #include "data/components/scheduled_messages.h" #include "data/components/sponsored_messages.h" @@ -105,6 +106,7 @@ Session::Session( , _scheduledMessages(std::make_unique(this)) , _sponsoredMessages(std::make_unique(this)) , _topPeers(std::make_unique(this)) +, _factchecks(std::make_unique(this)) , _cachedReactionIconFactory(std::make_unique()) , _supportHelper(Support::Helper::Create(this)) , _saveSettingsTimer([=] { saveSettings(); }) { diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index 85aa2fe08..9581e7cd4 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -35,6 +35,7 @@ class RecentPeers; class ScheduledMessages; class SponsoredMessages; class TopPeers; +class Factchecks; } // namespace Data namespace HistoryView::Reactions { @@ -127,6 +128,9 @@ public: [[nodiscard]] Data::TopPeers &topPeers() const { return *_topPeers; } + [[nodiscard]] Data::Factchecks &factchecks() const { + return *_factchecks; + } [[nodiscard]] Api::Updates &updates() const { return *_updates; } @@ -254,6 +258,7 @@ private: const std::unique_ptr _scheduledMessages; const std::unique_ptr _sponsoredMessages; const std::unique_ptr _topPeers; + const std::unique_ptr _factchecks; using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory; const std::unique_ptr _cachedReactionIconFactory;