diff --git a/Telegram/Resources/icons/menu/factcheck.png b/Telegram/Resources/icons/menu/factcheck.png new file mode 100644 index 000000000..e9115a4cf Binary files /dev/null and b/Telegram/Resources/icons/menu/factcheck.png differ diff --git a/Telegram/Resources/icons/menu/factcheck@2x.png b/Telegram/Resources/icons/menu/factcheck@2x.png new file mode 100644 index 000000000..bcc80e45e Binary files /dev/null and b/Telegram/Resources/icons/menu/factcheck@2x.png differ diff --git a/Telegram/Resources/icons/menu/factcheck@3x.png b/Telegram/Resources/icons/menu/factcheck@3x.png new file mode 100644 index 000000000..139e30eb8 Binary files /dev/null and b/Telegram/Resources/icons/menu/factcheck@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 6b28b450e..efb548724 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3217,6 +3217,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_reply_msg" = "Reply"; "lng_context_quote_and_reply" = "Quote & Reply"; "lng_context_edit_msg" = "Edit"; +"lng_context_add_factcheck" = "Add Fact Check"; +"lng_context_edit_factcheck" = "Edit Fact Check"; "lng_context_forward_msg" = "Forward Message"; "lng_context_send_now_msg" = "Send now"; "lng_context_reschedule" = "Reschedule"; @@ -3287,12 +3289,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_spoiler_effect" = "Hide with Spoiler"; "lng_context_disable_spoiler" = "Remove Spoiler"; -"lng_context_add_factcheck" = "Add Fact Check"; "lng_factcheck_title" = "Fact Check"; "lng_factcheck_placeholder" = "Add Facts or Context"; "lng_factcheck_whats_this" = "what's this?"; "lng_factcheck_about" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country ({country}) responsible for combatting misinformation."; +"lng_factcheck_add_done" = "Fact check added."; +"lng_factcheck_edit_done" = "Fact check edited."; +"lng_factcheck_remove_done" = "Fact check removed."; "lng_translate_show_original" = "Show Original"; "lng_translate_bar_to" = "Translate to {name}"; diff --git a/Telegram/SourceFiles/data/components/factchecks.cpp b/Telegram/SourceFiles/data/components/factchecks.cpp index 0423d1d43..af72d8aff 100644 --- a/Telegram/SourceFiles/data/components/factchecks.cpp +++ b/Telegram/SourceFiles/data/components/factchecks.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/components/factchecks.h" +#include "api/api_text_entities.h" #include "apiwrap.h" #include "base/random.h" #include "data/data_session.h" @@ -17,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_item_components.h" #include "lang/lang_keys.h" +#include "main/main_app_config.h" #include "main/main_session.h" namespace Data { @@ -135,4 +137,63 @@ std::unique_ptr Factchecks::makeMedia( MediaWebPageFlags()); } +bool Factchecks::canEdit(not_null item) const { + if (!canEdit() + || !item->isRegular() + || !item->history()->peer->isBroadcast()) { + return false; + } + const auto media = item->media(); + if (!media || media->webpage() || media->photo()) { + return true; + } else if (const auto document = media->document()) { + return !document->isVideoMessage() && !document->sticker(); + } + return false; +} + +bool Factchecks::canEdit() const { + return _session->appConfig().get(u"can_edit_factcheck"_q, false); +} + +int Factchecks::lengthLimit() const { + return _session->appConfig().get(u"factcheck_length_limit"_q, 1024); +} + +void Factchecks::save( + FullMsgId itemId, + TextWithEntities text, + Fn done) { + const auto item = _session->data().message(itemId); + if (!item) { + return; + } else if (text.empty()) { + _session->api().request(MTPmessages_DeleteFactCheck( + item->history()->peer->input, + MTP_int(item->id.bare) + )).done([=](const MTPUpdates &result) { + _session->api().applyUpdates(result); + done(QString()); + }).fail([=](const MTP::Error &error) { + done(error.type()); + }).send(); + } else { + _session->api().request(MTPmessages_EditFactCheck( + item->history()->peer->input, + MTP_int(item->id.bare), + MTP_textWithEntities( + MTP_string(text.text), + Api::EntitiesToMTP( + _session, + text.entities, + Api::ConvertOption::SkipLocal)) + )).done([=](const MTPUpdates &result) { + _session->api().applyUpdates(result); + done(QString()); + }).fail([=](const MTP::Error &error) { + done(error.type()); + }).send(); + } +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/components/factchecks.h b/Telegram/SourceFiles/data/components/factchecks.h index 452706f9b..7054a3c57 100644 --- a/Telegram/SourceFiles/data/components/factchecks.h +++ b/Telegram/SourceFiles/data/components/factchecks.h @@ -32,7 +32,17 @@ public: not_null view, not_null factcheck); + [[nodiscard]] bool canEdit(not_null item) const; + [[nodiscard]] int lengthLimit() const; + + void save( + FullMsgId itemId, + TextWithEntities text, + Fn done); + private: + [[nodiscard]] bool canEdit() const; + void subscribeIfNotYet(); void request(); 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 8fa440bf6..cb099afe4 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -1306,8 +1306,7 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { && !link && (view->hasVisibleText() || mediaHasTextForCopy - || (item->Has() - && !item->Get()->data.text.empty()) + || !item->factcheckText().empty() || item->Has())) { _menu->addAction(tr::lng_context_copy_text(tr::now), [=] { copyContextText(itemId); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 1687b5049..e6950859d 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/reaction_fly_animation.h" #include "ui/text/text_options.h" #include "ui/text/text_isolated_emoji.h" +#include "ui/boxes/edit_factcheck_box.h" #include "ui/boxes/report_box.h" #include "ui/layers/generic_box.h" #include "ui/controls/delete_message_context_action.h" @@ -72,6 +73,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_who_reacted.h" #include "api/api_views.h" #include "lang/lang_keys.h" +#include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" #include "data/data_session.h" #include "data/data_document.h" @@ -2171,6 +2173,30 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } }, &st::menuIconEdit); } + if (session->factchecks().canEdit(item)) { + const auto text = item->factcheckText(); + const auto phrase = text.empty() + ? tr::lng_context_add_factcheck(tr::now) + : tr::lng_context_edit_factcheck(tr::now); + _menu->addAction(phrase, [=] { + controller->show(Box(EditFactcheckBox, text, [=]( + TextWithEntities result) { + const auto done = [=](QString error) { + controller->showToast(!error.isEmpty() + ? error + : result.empty() + ? tr::lng_factcheck_remove_done(tr::now) + : text.empty() + ? tr::lng_factcheck_add_done(tr::now) + : tr::lng_factcheck_edit_done(tr::now)); + }; + session->factchecks().save( + itemId, + result, + crl::guard(controller, done)); + })); + }, &st::menuIconFactcheck); + } const auto pinItem = (item->canPin() && item->isPinned()) ? item : groupLeaderOrSelf(item); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 3a2ed484a..30cc8a9c3 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1508,14 +1508,18 @@ void HistoryItem::setFactcheck(MessageFactcheck info) { } else { AddComponents(HistoryMessageFactcheck::Bit()); const auto factcheck = Get(); + const auto textChanged = (factcheck->data.text != info.text); if (factcheck->data.hash == info.hash && (info.needCheck || !factcheck->data.needCheck)) { return; - } else if (factcheck->data.text != info.text + } else if (textChanged || factcheck->data.country != info.country || factcheck->data.hash != info.hash) { factcheck->data = std::move(info); factcheck->requested = false; + if (textChanged) { + factcheck->page = nullptr; + } history()->owner().requestItemResize(this); } } @@ -1526,6 +1530,13 @@ bool HistoryItem::hasUnrequestedFactcheck() const { return factcheck && factcheck->data.needCheck && !factcheck->requested; } +TextWithEntities HistoryItem::factcheckText() const { + if (const auto factcheck = Get()) { + return factcheck->data.text; + } + return {}; +} + PeerData *HistoryItem::specialNotificationPeer() const { return (mentionsMe() && !_history->peer->isUser()) ? from().get() @@ -1725,6 +1736,7 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { } applyTTL(edition.ttl); + setFactcheck(FromMTP(this, edition.mtpFactcheck)); finishEdition(keyboardTop); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 5a864d076..3118bf3bc 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -208,6 +208,7 @@ public: const TextWithEntities &content); void setFactcheck(MessageFactcheck info); [[nodiscard]] bool hasUnrequestedFactcheck() const; + [[nodiscard]] TextWithEntities factcheckText() 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 54e42a16e..fd1a04942 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -1065,6 +1065,12 @@ HistoryMessageLogEntryOriginal::~HistoryMessageLogEntryOriginal() = default; MessageFactcheck FromMTP( not_null item, const tl::conditional &factcheck) { + return FromMTP(&item->history()->session(), factcheck); +} + +MessageFactcheck FromMTP( + not_null session, + const tl::conditional &factcheck) { auto result = MessageFactcheck(); if (!factcheck) { return result; @@ -1074,9 +1080,7 @@ MessageFactcheck FromMTP( const auto &data = text->data(); result.text = { qs(data.vtext()), - Api::EntitiesFromMTP( - &item->history()->session(), - data.ventities().v), + Api::EntitiesFromMTP(session, data.ventities().v), }; } if (const auto country = data.vcountry()) { diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index fa7ffbf3a..906fe18ce 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL struct WebPageData; class VoiceSeekClickHandler; +class ReplyKeyboard; namespace Ui { struct ChatPaintContext; @@ -31,6 +32,7 @@ struct GeometryDescriptor; namespace Data { class Session; class Story; +class SavedSublist; } // namespace Data namespace Media::Player { @@ -47,6 +49,10 @@ class Document; class TranscribeButton; } // namespace HistoryView +namespace style { +struct BotKeyboardButton; +} // namespace style + struct HistoryMessageVia : public RuntimeComponent { void create(not_null owner, UserId userId); void resize(int32 availw) const; @@ -579,6 +585,9 @@ struct MessageFactcheck { [[nodiscard]] MessageFactcheck FromMTP( not_null item, const tl::conditional &factcheck); +[[nodiscard]] MessageFactcheck FromMTP( + not_null session, + const tl::conditional &factcheck); struct HistoryMessageFactcheck : public RuntimeComponent { diff --git a/Telegram/SourceFiles/history/history_item_edition.cpp b/Telegram/SourceFiles/history/history_item_edition.cpp index 4349a3c93..d66d0cf7b 100644 --- a/Telegram/SourceFiles/history/history_item_edition.cpp +++ b/Telegram/SourceFiles/history/history_item_edition.cpp @@ -24,6 +24,7 @@ HistoryMessageEdition::HistoryMessageEdition( replyMarkup = HistoryMessageMarkupData(message.vreply_markup()); mtpMedia = message.vmedia(); mtpReactions = message.vreactions(); + mtpFactcheck = message.vfactcheck(); views = message.vviews().value_or(-1); forwards = message.vforwards().value_or(-1); if (const auto mtpReplies = message.vreplies()) { diff --git a/Telegram/SourceFiles/history/history_item_edition.h b/Telegram/SourceFiles/history/history_item_edition.h index b8f577d80..a44109299 100644 --- a/Telegram/SourceFiles/history/history_item_edition.h +++ b/Telegram/SourceFiles/history/history_item_edition.h @@ -36,4 +36,5 @@ struct HistoryMessageEdition { HistoryMessageRepliesData replies; const MTPMessageMedia *mtpMedia = nullptr; const MTPMessageReactions *mtpReactions = nullptr; + const MTPFactCheck *mtpFactcheck = nullptr; }; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 564ea3b7b..b860c2c58 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -917,8 +917,8 @@ QSize Message::performCountOptimalSize() { minHeight += st::msgPadding.top(); if (mediaDisplayed) minHeight += st::mediaInBubbleSkip; if (entry) minHeight += st::mediaInBubbleSkip; - if (check) minHeight += st::mediaInBubbleSkip; } + if (check) minHeight += st::mediaInBubbleSkip; if (mediaDisplayed) { // Parts don't participate in maxWidth() in case of media message. if (media->enforceBubbleWidth()) { @@ -1308,7 +1308,7 @@ void Message::draw(Painter &p, const PaintContext &context) const { trect.setHeight(trect.height() - entry->height()); } if (check) { - trect.setHeight(trect.height() - check->height()); + trect.setHeight(trect.height() - check->height() - st::mediaInBubbleSkip); } if (displayInfo) { trect.setHeight(trect.height() @@ -1371,7 +1371,7 @@ void Message::draw(Painter &p, const PaintContext &context) const { } if (check) { auto checkLeft = inner.left(); - auto checkTop = trect.y() + trect.height(); + auto checkTop = trect.y() + trect.height() + st::mediaInBubbleSkip; p.translate(checkLeft, checkTop); auto checkContext = context.translated(checkLeft, -checkTop); checkContext.selection = skipTextSelection(context.selection); @@ -1986,7 +1986,7 @@ PointState Message::pointState(QPoint point) const { //} if (check) { auto checkHeight = check->height(); - trect.setHeight(trect.height() - checkHeight); + trect.setHeight(trect.height() - checkHeight - st::mediaInBubbleSkip); } if (entry) { auto entryHeight = entry->height(); @@ -2428,9 +2428,9 @@ TextState Message::textState( } if (check) { auto checkHeight = check->height(); - trect.setHeight(trect.height() - checkHeight); + trect.setHeight(trect.height() - checkHeight - st::mediaInBubbleSkip); auto checkLeft = inner.left(); - auto checkTop = trect.y() + trect.height(); + auto checkTop = trect.y() + trect.height() + st::mediaInBubbleSkip; if (point.y() >= checkTop && point.y() < checkTop + checkHeight) { result = check->textState( point - QPoint(checkLeft, checkTop), @@ -4333,7 +4333,7 @@ int Message::resizeContentGetHeight(int newWidth) { if (contentWidth == maxWidth()) { if (mediaDisplayed) { if (check) { - newHeight += check->resizeGetHeight(contentWidth); + newHeight += check->resizeGetHeight(contentWidth) + st::mediaInBubbleSkip; } if (entry) { newHeight += entry->resizeGetHeight(contentWidth); @@ -4365,24 +4365,16 @@ int Message::resizeContentGetHeight(int newWidth) { if (!mediaOnTop) { newHeight += st::msgPadding.top(); if (mediaDisplayed) newHeight += st::mediaInBubbleSkip; - if (check) newHeight += st::mediaInBubbleSkip; if (entry) newHeight += st::mediaInBubbleSkip; } if (mediaDisplayed) { newHeight += media->height(); - if (check) { - newHeight += check->resizeGetHeight(contentWidth); - } - if (entry) { - newHeight += entry->resizeGetHeight(contentWidth); - } - } else { - if (check) { - newHeight += check->resizeGetHeight(contentWidth); - } - if (entry) { - newHeight += entry->resizeGetHeight(contentWidth); - } + } + if (check) { + newHeight += check->resizeGetHeight(contentWidth) + st::mediaInBubbleSkip; + } + if (entry) { + newHeight += entry->resizeGetHeight(contentWidth); } if (reactionsInBubble) { if (!mediaDisplayed || _viewButton) { @@ -4518,7 +4510,8 @@ void Message::refreshInfoSkipBlock() { return media->storyExpired(); } return false; - } else if (item->Has()) { + } else if (item->Has() + || factcheckBlock()) { return false; } else if (media && media->isDisplayed() && !_invertMedia) { return false; diff --git a/Telegram/SourceFiles/ui/boxes/edit_factcheck_box.cpp b/Telegram/SourceFiles/ui/boxes/edit_factcheck_box.cpp new file mode 100644 index 000000000..f13c3e976 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/edit_factcheck_box.cpp @@ -0,0 +1,81 @@ +/* +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 "ui/boxes/edit_factcheck_box.h" + +#include "lang/lang_keys.h" +#include "ui/widgets/fields/input_field.h" +#include "styles/style_chat.h" +#include "styles/style_layers.h" + +void EditFactcheckBox( + not_null box, + TextWithEntities current, + Fn save) { + box->setTitle(tr::lng_factcheck_title()); + + const auto field = box->addRow(object_ptr( + box, + st::factcheckField, + Ui::InputField::Mode::NoNewlines, + tr::lng_factcheck_placeholder(), + TextWithTags{ + current.text, + TextUtilities::ConvertEntitiesToTextTags(current.entities) + })); + + enum class State { + Initial, + Changed, + Removed, + }; + const auto state = box->lifetime().make_state>( + State::Initial); + field->changes() | rpl::start_with_next([=] { + const auto now = field->getLastText().trimmed(); + *state = !now.isEmpty() + ? State::Changed + : current.empty() + ? State::Initial + : State::Removed; + }, field->lifetime()); + + state->value() | rpl::start_with_next([=](State state) { + box->clearButtons(); + if (state == State::Removed) { + box->addButton(tr::lng_box_remove(), [=] { + box->closeBox(); + save({}); + }, st::attentionBoxButton); + } else if (state == State::Initial) { + box->addButton(tr::lng_settings_save(), [=] { + if (current.empty()) { + field->showError(); + } else { + box->closeBox(); + } + }); + } else { + box->addButton(tr::lng_settings_save(), [=] { + auto result = field->getTextWithAppliedMarkdown(); + + box->closeBox(); + save({ + result.text, + TextUtilities::ConvertTextTagsToEntities(result.tags) + }); + }); + } + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + }, box->lifetime()); + + box->setFocusCallback([=] { + field->setFocusFast(); + }); +} diff --git a/Telegram/SourceFiles/ui/boxes/edit_factcheck_box.h b/Telegram/SourceFiles/ui/boxes/edit_factcheck_box.h new file mode 100644 index 000000000..f09f52eb4 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/edit_factcheck_box.h @@ -0,0 +1,15 @@ +/* +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 "ui/layers/generic_box.h" + +void EditFactcheckBox( + not_null box, + TextWithEntities current, + Fn save); diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 6c398f236..9dbf43303 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1135,3 +1135,18 @@ effectPreviewLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) { factcheckIconExpand: icon {{ "fast_to_original-rotate_cw", historyPeer1NameFg }}; factcheckIconCollapse: icon {{ "fast_to_original-rotate_ccw", historyPeer1NameFg }}; +factcheckField: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(0px, 0px, 0px, 4px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + placeholderFont: normalFont; + + heightMin: 24px; + + font: normalFont; +} diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index eee9919fd..8f2c0f861 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -153,6 +153,7 @@ menuIconTagFilter: icon{{ "menu/tag_filter", menuIconColor }}; menuIconTagRename: icon{{ "menu/tag_rename", menuIconColor }}; menuIconGroupsHide: icon {{ "menu/hide_members", menuIconColor }}; menuIconFont: icon {{ "menu/fonts", menuIconColor }}; +menuIconFactcheck: icon {{ "menu/factcheck", menuIconColor }}; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAnyTextPosition: point(11px, 22px); diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 0a97c6b6d..3c0c026a3 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -261,6 +261,8 @@ PRIVATE ui/boxes/country_select_box.h ui/boxes/edit_birthday_box.cpp ui/boxes/edit_birthday_box.h + ui/boxes/edit_factcheck_box.cpp + ui/boxes/edit_factcheck_box.h ui/boxes/edit_invite_link.cpp ui/boxes/edit_invite_link.h ui/boxes/rate_call_box.cpp