Track factcheck text and create media.

This commit is contained in:
John Preston 2024-05-21 18:15:56 +04:00
parent 5ee2bca616
commit b299881bf8
13 changed files with 332 additions and 11 deletions

View file

@ -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<Main::Session*> session)
: _session(session)
, _requestTimer([=] { request(); }) {
}
void Factchecks::requestFor(not_null<HistoryItem*> item) {
subscribeIfNotYet();
if (const auto factcheck = item->Get<HistoryMessageFactcheck>()) {
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<const HistoryItem*> 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<MTPint>();
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<MTPint>(std::move(ids))
)).done([=](const MTPVector<MTPFactCheck> &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<HistoryView::WebPage> Factchecks::makeMedia(
not_null<HistoryView::Message*> view,
not_null<HistoryMessageFactcheck*> factcheck) {
if (!factcheck->page) {
factcheck->page = view->history()->owner().webpage(
base::RandomValue<WebPageId>(),
tr::lng_factcheck_title(tr::now),
factcheck->data.text);
}
return std::make_unique<HistoryView::WebPage>(
view,
factcheck->page,
MediaWebPageFlags());
}
} // namespace Data

View file

@ -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<Main::Session*> session);
void requestFor(not_null<HistoryItem*> item);
[[nodiscard]] std::unique_ptr<HistoryView::WebPage> makeMedia(
not_null<HistoryView::Message*> view,
not_null<HistoryMessageFactcheck*> factcheck);
private:
void subscribeIfNotYet();
void request();
const not_null<Main::Session*> _session;
base::Timer _requestTimer;
base::flat_set<not_null<HistoryItem*>> _pending;
std::vector<HistoryItem*> _requested;
mtpRequestId _requestId = 0;
bool _subscribed = false;
rpl::lifetime _lifetime;
};
} // namespace Data

View file

@ -4250,29 +4250,27 @@ void Session::notifyPollUpdateDelayed(not_null<PollData*> poll) {
}
void Session::sendWebPageGamePollNotifications() {
auto resize = std::vector<not_null<ViewElement*>>();
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<not_null<WebPageData*>> Session::webPageUpdates() const {

View file

@ -1306,6 +1306,8 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
&& !link
&& (view->hasVisibleText()
|| mediaHasTextForCopy
|| (item->Has<HistoryMessageFactcheck>()
&& !item->Get<HistoryMessageFactcheck>()->data.text.empty())
|| item->Has<HistoryMessageLogEntryOriginal>())) {
_menu->addAction(tr::lng_context_copy_text(tr::now), [=] {
copyContextText(itemId);

View file

@ -415,6 +415,11 @@ HistoryItem::HistoryItem(
}
setReactions(data.vreactions());
applyTTL(data);
if (const auto check = FromMTP(this, data.vfactcheck())) {
AddComponents(HistoryMessageFactcheck::Bit());
Get<HistoryMessageFactcheck>()->data = check;
}
}
}
@ -1494,6 +1499,33 @@ void HistoryItem::addLogEntryOriginal(
content);
}
void HistoryItem::setFactcheck(MessageFactcheck info) {
if (!info) {
if (Has<HistoryMessageFactcheck>()) {
RemoveComponents(HistoryMessageFactcheck::Bit());
history()->owner().requestItemResize(this);
}
} else {
AddComponents(HistoryMessageFactcheck::Bit());
const auto factcheck = Get<HistoryMessageFactcheck>();
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<HistoryMessageFactcheck>();
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<HistoryMessageFactcheck>()
|| Get<HistoryMessageFactcheck>()->data.text.empty())
&& !Has<HistoryMessageLogEntryOriginal>();
}

View file

@ -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<Data::Thread*> notificationThread() const;
[[nodiscard]] not_null<History*> history() const {

View file

@ -1062,6 +1062,31 @@ HistoryMessageLogEntryOriginal &HistoryMessageLogEntryOriginal::operator=(
HistoryMessageLogEntryOriginal::~HistoryMessageLogEntryOriginal() = default;
MessageFactcheck FromMTP(
not_null<HistoryItem*> item,
const tl::conditional<MTPFactCheck> &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()) {
}

View file

@ -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<HistoryItem*> item,
const tl::conditional<MTPFactCheck> &factcheck);
struct HistoryMessageFactcheck
: public RuntimeComponent<HistoryMessageFactcheck, HistoryItem> {
MessageFactcheck data;
WebPageData *page = nullptr;
bool requested = false;
};
struct HistoryServiceData
: public RuntimeComponent<HistoryServiceData, HistoryItem> {
std::vector<ClickHandlerPtr> textLinks;

View file

@ -46,6 +46,12 @@ TextForMimeData HistoryItemText(not_null<HistoryItem*> item) {
titleResult.append('\n').append(std::move(descriptionResult));
return titleResult;
}();
auto factcheckResult = [&] {
const auto factcheck = item->Get<HistoryMessageFactcheck>();
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<HistoryItem*> 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;
}

View file

@ -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<HistoryMessageFactcheck>();
if (factcheck && !factcheck->data.text.empty()) {
AddComponents(Factcheck::Bit());
Get<Factcheck>()->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();

View file

@ -44,6 +44,11 @@ struct LogEntryOriginal
std::unique_ptr<WebPage> page;
};
struct Factcheck
: public RuntimeComponent<Factcheck, Element> {
std::unique_ptr<WebPage> page;
};
struct PsaTooltipState : public RuntimeComponent<PsaTooltipState, Element> {
QString type;
mutable ClickHandlerPtr link;

View file

@ -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<Data::ScheduledMessages>(this))
, _sponsoredMessages(std::make_unique<Data::SponsoredMessages>(this))
, _topPeers(std::make_unique<Data::TopPeers>(this))
, _factchecks(std::make_unique<Data::Factchecks>(this))
, _cachedReactionIconFactory(std::make_unique<ReactionIconFactory>())
, _supportHelper(Support::Helper::Create(this))
, _saveSettingsTimer([=] { saveSettings(); }) {

View file

@ -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<Data::ScheduledMessages> _scheduledMessages;
const std::unique_ptr<Data::SponsoredMessages> _sponsoredMessages;
const std::unique_ptr<Data::TopPeers> _topPeers;
const std::unique_ptr<Data::Factchecks> _factchecks;
using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory;
const std::unique_ptr<ReactionIconFactory> _cachedReactionIconFactory;