diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 8d1d2369d..a562048ec 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -466,6 +466,8 @@ PRIVATE data/business/data_business_info.h data/business/data_shortcut_messages.cpp data/business/data_shortcut_messages.h + data/components/factchecks.cpp + data/components/factchecks.h data/components/recent_peers.cpp data/components/recent_peers.h data/components/scheduled_messages.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 8cf16783d..6b28b450e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3287,6 +3287,12 @@ 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_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 4e5fcd090..0423d1d43 100644 --- a/Telegram/SourceFiles/data/components/factchecks.cpp +++ b/Telegram/SourceFiles/data/components/factchecks.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/random.h" #include "data/data_session.h" +#include "data/data_web_page.h" #include "history/view/media/history_view_web_page.h" #include "history/view/history_view_message.h" #include "history/history.h" @@ -126,6 +127,7 @@ std::unique_ptr Factchecks::makeMedia( base::RandomValue(), tr::lng_factcheck_title(tr::now), factcheck->data.text); + factcheck->page->type = WebPageType::Factcheck; } return std::make_unique( view, diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h index 8b82ed859..6497a0394 100644 --- a/Telegram/SourceFiles/data/data_web_page.h +++ b/Telegram/SourceFiles/data/data_web_page.h @@ -54,6 +54,8 @@ enum class WebPageType : uint8 { VoiceChat, Livestream, + + Factcheck, }; [[nodiscard]] WebPageType ParseWebPageType(const MTPDwebPage &type); [[nodiscard]] bool IgnoreIv(WebPageType type); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 5e46d8d97..19f76be07 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -111,7 +111,8 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { | MTPDmessage::Flag::f_forwards //| MTPDmessage::Flag::f_reactions | MTPDmessage::Flag::f_restriction_reason - | MTPDmessage::Flag::f_ttl_period; + | MTPDmessage::Flag::f_ttl_period + | MTPDmessage::Flag::f_factcheck; return MTP_message( MTP_flags(data.vflags().v & ~removeFlags), data.vid(), @@ -141,7 +142,7 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { MTPint(), // ttl_period MTPint(), // quick_reply_shortcut_id MTP_long(data.veffect().value_or_empty()), - data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck()); + MTPFactCheck()); }); } diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 861c96ee7..564ea3b7b 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -586,10 +586,11 @@ void Message::animateReaction(Ui::ReactionFlyAnimationArgs &&args) { } if (bubble) { - auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + const auto entry = logEntryOriginal(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); auto inner = g; @@ -636,10 +637,11 @@ void Message::animateEffect(Ui::ReactionFlyAnimationArgs &&args) { _bottomInfo.animateEffect(args.translated(-bottomRight), repainter); }; if (bubble) { - auto entry = logEntryOriginal(); + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); auto inner = g; @@ -732,10 +734,11 @@ QRect Message::effectIconGeometry() const { bottomRight - QPoint(size.width(), size.height())); }; if (bubble) { - auto entry = logEntryOriginal(); + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); auto inner = g; @@ -806,14 +809,6 @@ QSize Message::performCountOptimalSize() { 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()); } @@ -863,6 +858,7 @@ QSize Message::performCountOptimalSize() { const auto forwarded = item->Get(); const auto via = item->Get(); const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); if (forwarded) { forwarded->create(via, item); } @@ -872,13 +868,16 @@ QSize Message::performCountOptimalSize() { mediaDisplayed = media->isDisplayed(); media->initDimensions(); } + if (check) { + check->initDimensions(); + } if (entry) { entry->initDimensions(); } // Entry page is always a bubble bottom. const auto withVisibleText = hasVisibleText(); - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); maxWidth = plainMaxWidth(); if (context() == Context::Replies && item->isDiscussionPost()) { @@ -918,6 +917,7 @@ QSize Message::performCountOptimalSize() { minHeight += st::msgPadding.top(); if (mediaDisplayed) minHeight += st::mediaInBubbleSkip; if (entry) minHeight += st::mediaInBubbleSkip; + if (check) minHeight += st::mediaInBubbleSkip; } if (mediaDisplayed) { // Parts don't participate in maxWidth() in case of media message. @@ -1002,6 +1002,10 @@ QSize Message::performCountOptimalSize() { + st::msgPadding.right(); accumulate_max(maxWidth, replyw); } + if (check) { + accumulate_max(maxWidth, check->maxWidth()); + minHeight += check->minHeight(); + } if (entry) { accumulate_max(maxWidth, entry->maxWidth()); minHeight += entry->minHeight(); @@ -1121,11 +1125,12 @@ void Message::draw(Painter &p, const PaintContext &context) const { return; } - auto entry = logEntryOriginal(); + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); auto mediaDisplayed = media && media->isDisplayed(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); const auto displayInfo = needInfoDisplay(); @@ -1150,6 +1155,9 @@ void Message::draw(Painter &p, const PaintContext &context) const { if (!mediaOnBottom && (!_viewButton || !reactionsInBubble)) { localMediaBottom -= st::msgPadding.bottom(); } + if (check) { + localMediaBottom -= check->height(); + } if (entry) { localMediaBottom -= entry->height(); } @@ -1299,6 +1307,9 @@ void Message::draw(Painter &p, const PaintContext &context) const { if (entry) { trect.setHeight(trect.height() - entry->height()); } + if (check) { + trect.setHeight(trect.height() - check->height()); + } if (displayInfo) { trect.setHeight(trect.height() - (_bottomInfo.height() - st::msgDateFont->height)); @@ -1358,6 +1369,19 @@ void Message::draw(Painter &p, const PaintContext &context) const { } } } + if (check) { + auto checkLeft = inner.left(); + auto checkTop = trect.y() + trect.height(); + p.translate(checkLeft, checkTop); + auto checkContext = context.translated(checkLeft, -checkTop); + checkContext.selection = skipTextSelection(context.selection); + if (mediaDisplayed) { + checkContext.selection = media->skipSelection( + checkContext.selection); + } + check->draw(p, checkContext); + p.translate(-checkLeft, -checkTop); + } if (entry) { auto entryLeft = inner.left(); auto entryTop = trect.y() + trect.height(); @@ -1924,10 +1948,11 @@ PointState Message::pointState(QPoint point) const { } if (const auto mediaDisplayed = media && media->isDisplayed()) { // Hack for grouped media point state. - auto entry = logEntryOriginal(); + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); if (item->repliesAreComments() || item->externalReply()) { g.setHeight(g.height() - st::historyCommentsButtonHeight); @@ -1959,6 +1984,10 @@ PointState Message::pointState(QPoint point) const { // if (getStateReplyInfo(point, trect, &result)) return result; // if (getStateViaBotIdInfo(point, trect, &result)) return result; //} + if (check) { + auto checkHeight = check->height(); + trect.setHeight(trect.height() - checkHeight); + } if (entry) { auto entryHeight = entry->height(); trect.setHeight(trect.height() - entryHeight); @@ -1995,6 +2024,9 @@ void Message::clickHandlerPressedChanged( } } Element::clickHandlerPressedChanged(handler, pressed); + if (const auto check = factcheckBlock()) { + check->clickHandlerPressedChanged(handler, pressed); + } if (!handler) { return; } else if (_rightAction && (handler == _rightAction->link)) { @@ -2306,10 +2338,11 @@ TextState Message::textState( if (bubble) { const auto inBubble = g.contains(point); - auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + const auto entry = logEntryOriginal(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); auto inner = g; @@ -2393,9 +2426,23 @@ TextState Message::textState( + visibleMediaTextLength(); } } + if (check) { + auto checkHeight = check->height(); + trect.setHeight(trect.height() - checkHeight); + auto checkLeft = inner.left(); + auto checkTop = trect.y() + trect.height(); + if (point.y() >= checkTop && point.y() < checkTop + checkHeight) { + result = check->textState( + point - QPoint(checkLeft, checkTop), + request); + result.symbol += visibleTextLength() + + visibleMediaTextLength(); + } + } auto checkBottomInfoState = [&] { - if (mediaOnBottom && (entry || media->customInfoLayout())) { + if (mediaOnBottom + && (check || entry || media->customInfoLayout())) { return; } const auto bottomInfoResult = bottomInfoTextState( @@ -2862,6 +2909,7 @@ void Message::updatePressed(QPoint point) { TextForMimeData Message::selectedText(TextSelection selection) const { const auto media = this->media(); auto logEntryOriginalResult = TextForMimeData(); + auto factcheckResult = TextForMimeData(); const auto mediaDisplayed = (media && media->isDisplayed()); const auto mediaBefore = mediaDisplayed && invertMedia(); const auto textSelection = mediaBefore @@ -2876,7 +2924,15 @@ TextForMimeData Message::selectedText(TextSelection selection) const { auto mediaResult = (mediaDisplayed || isHiddenByGroup()) ? media->selectedText(mediaSelection) : TextForMimeData(); - if (auto entry = logEntryOriginal()) { + if (const auto check = factcheckBlock()) { + const auto checkSelection = mediaBefore + ? skipTextSelection(textSelection) + : mediaDisplayed + ? media->skipSelection(mediaSelection) + : skipTextSelection(selection); + factcheckResult = check->selectedText(checkSelection); + } + if (const auto entry = logEntryOriginal()) { const auto originalSelection = mediaBefore ? skipTextSelection(textSelection) : mediaDisplayed @@ -2892,6 +2948,11 @@ TextForMimeData Message::selectedText(TextSelection selection) const { } else if (!second.empty()) { result.append(u"\n\n"_q).append(std::move(second)); } + if (result.empty()) { + result = std::move(factcheckResult); + } else if (!factcheckResult.empty()) { + result.append(u"\n\n"_q).append(std::move(factcheckResult)); + } if (result.empty()) { result = std::move(logEntryOriginalResult); } else if (!logEntryOriginalResult.empty()) { @@ -2981,6 +3042,21 @@ TextSelection Message::adjustSelection( ? mediaAdjusted : unskipTextSelection(mediaAdjusted); } + auto checkResult = TextSelection(); + if (const auto check = factcheckBlock()) { + auto checkSelection = !mediaDisplayed + ? skipTextSelection(selection) + : mediaBefore + ? skipTextSelection(textSelection) + : media->skipSelection(mediaSelection); + auto checkAdjusted = useSelection(checkSelection, true) + ? check->adjustSelection(checkSelection, type) + : checkSelection; + checkResult = unskipTextSelection(checkAdjusted); + if (mediaDisplayed) { + checkResult = media->unskipSelection(checkResult); + } + } auto entryResult = TextSelection(); if (const auto entry = logEntryOriginal()) { auto entrySelection = !mediaDisplayed @@ -3003,6 +3079,12 @@ TextSelection Message::adjustSelection( std::max(result.to, mediaResult.to), }; } + if (!checkResult.empty()) { + result = result.empty() ? checkResult : TextSelection{ + std::min(result.from, checkResult.from), + std::max(result.to, checkResult.to), + }; + } if (!entryResult.empty()) { result = result.empty() ? entryResult : TextSelection{ std::min(result.from, entryResult.from), @@ -3410,6 +3492,13 @@ WebPage *Message::logEntryOriginal() const { return nullptr; } +WebPage *Message::factcheckBlock() const { + if (const auto entry = Get()) { + return entry->page.get(); + } + return nullptr; +} + bool Message::toggleSelectionByHandlerClick( const ClickHandlerPtr &handler) const { if (_comments && _comments->link == handler) { @@ -3539,7 +3628,9 @@ bool Message::drawBubble() const { const auto item = data(); if (isHidden()) { return false; - } else if (logEntryOriginal() || item->isFakeAboutView()) { + } else if (logEntryOriginal() + || factcheckBlock() + || item->isFakeAboutView()) { return true; } const auto media = this->media(); @@ -3560,7 +3651,7 @@ bool Message::unwrapped() const { const auto item = data(); if (isHidden()) { return true; - } else if (logEntryOriginal()) { + } else if (logEntryOriginal() || factcheckBlock()) { return false; } const auto media = this->media(); @@ -3921,8 +4012,22 @@ void Message::updateMediaInBubbleState() { || Has() || item->Has(); }; - auto entry = logEntryOriginal(); - if (entry) { + const auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + if (check) { + mediaHasSomethingBelow = true; + mediaHasSomethingAbove = getMediaHasSomethingAbove(); + auto checkState = (mediaHasSomethingAbove + || hasVisibleText() + || (media && media->isDisplayed())) + ? MediaInBubbleState::Bottom + : MediaInBubbleState::None; + check->setInBubbleState(checkState); + if (!media) { + check->setBubbleRounding(countBubbleRounding()); + return; + } + } else if (entry) { mediaHasSomethingBelow = true; mediaHasSomethingAbove = getMediaHasSomethingAbove(); auto entryState = (mediaHasSomethingAbove @@ -3947,7 +4052,7 @@ void Message::updateMediaInBubbleState() { return; } - if (!entry) { + if (!check && !entry) { mediaHasSomethingAbove = getMediaHasSomethingAbove(); } if (!invertMedia() && hasVisibleText()) { @@ -4214,10 +4319,11 @@ int Message::resizeContentGetHeight(int newWidth) { if (bubble) { auto reply = Get(); auto via = item->Get(); - auto entry = logEntryOriginal(); + const auto check = factcheckBlock(); + const auto entry = logEntryOriginal(); // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); if (reactionsInBubble) { @@ -4226,12 +4332,20 @@ int Message::resizeContentGetHeight(int newWidth) { if (contentWidth == maxWidth()) { if (mediaDisplayed) { + if (check) { + newHeight += check->resizeGetHeight(contentWidth); + } if (entry) { newHeight += entry->resizeGetHeight(contentWidth); } - } else if (entry) { - // In case of text-only message it is counted in minHeight already. - entry->resizeGetHeight(contentWidth); + } else { + if (check) { + check->resizeGetHeight(contentWidth); + } + if (entry) { + // In case of text-only message it is counted in minHeight already. + entry->resizeGetHeight(contentWidth); + } } } else { const auto withVisibleText = hasVisibleText(); @@ -4251,15 +4365,24 @@ 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); } - } else if (entry) { - newHeight += entry->resizeGetHeight(contentWidth); } if (reactionsInBubble) { if (!mediaDisplayed || _viewButton) { @@ -4343,9 +4466,12 @@ int Message::resizeContentGetHeight(int newWidth) { bool Message::needInfoDisplay() const { const auto media = this->media(); const auto mediaDisplayed = media ? media->isDisplayed() : false; + const auto check = factcheckBlock(); const auto entry = logEntryOriginal(); return entry ? !entry->customInfoLayout() + : check + ? !check->customInfoLayout() : ((mediaDisplayed && media->isBubbleBottom()) ? !media->customInfoLayout() : true); diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 5f64193d7..240e1a47a 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -47,6 +47,7 @@ struct LogEntryOriginal struct Factcheck : public RuntimeComponent { std::unique_ptr page; + bool expanded = false; }; struct PsaTooltipState : public RuntimeComponent { @@ -294,6 +295,7 @@ private: [[nodiscard]] int viewButtonHeight() const; [[nodiscard]] WebPage *logEntryOriginal() const; + [[nodiscard]] WebPage *factcheckBlock() const; [[nodiscard]] ClickHandlerPtr createGoToCommentsLink() const; [[nodiscard]] ClickHandlerPtr psaTooltipLink() const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index 0181d7da3..77d6fa5ea 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_web_page.h" #include "core/application.h" +#include "countries/countries_instance.h" #include "base/qt/qt_key_modifiers.h" #include "window/window_session_controller.h" #include "iv/iv_instance.h" @@ -19,13 +20,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_web_page.h" -#include "history/history.h" -#include "history/history_item_components.h" -#include "history/view/history_view_cursor_state.h" -#include "history/view/history_view_reply.h" -#include "history/view/history_view_sponsored_click_handler.h" #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_sticker.h" +#include "history/view/history_view_cursor_state.h" +#include "history/view/history_view_message.h" +#include "history/view/history_view_reply.h" +#include "history/view/history_view_sponsored_click_handler.h" +#include "history/history.h" +#include "history/history_item_components.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "menu/menu_sponsored.h" @@ -36,13 +38,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "styles/style_chat.h" namespace HistoryView { namespace { constexpr auto kMaxOriginalEntryLines = 8192; +constexpr auto kFactcheckCollapsedLines = 3; constexpr auto kStickerSetLines = 3; +constexpr auto kFactcheckAboutDuration = 5 * crl::time(1000); [[nodiscard]] int ArticleThumbWidth(not_null thumb, int height) { const auto size = thumb->location(Data::PhotoSize::Thumbnail); @@ -148,6 +153,39 @@ constexpr auto kStickerSetLines = 3; }); } +[[nodiscard]] ClickHandlerPtr AboutFactcheckClickHandler(QString iso2) { + return std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + const auto controller = my.sessionWindow.get(); + const auto show = my.show + ? my.show + : controller + ? controller->uiShow() + : nullptr; + if (show) { + const auto name = Countries::Instance().countryNameByISO2(iso2); + const auto use = name.isEmpty() ? iso2 : name; + show->showToast({ + .text = { tr::lng_factcheck_about(tr::now, lt_country, use) }, + .duration = kFactcheckAboutDuration, + }); + } + }); +} + +[[nodiscard]] ClickHandlerPtr ToggleFactcheckClickHandler( + not_null view) { + const auto weak = base::make_weak(view); + return std::make_shared([=](ClickContext context) { + if (const auto strong = weak.get()) { + if (const auto factcheck = strong->Get()) { + factcheck->expanded = !factcheck->expanded; + strong->history()->owner().requestViewResize(strong); + } + } + }); +} + [[nodiscard]] TextWithEntities PageToPhrase(not_null page) { const auto type = page->type; const auto text = Ui::Text::Upper(page->iv @@ -234,32 +272,55 @@ WebPage::WebPage( : Media(parent) , _st(st::historyPagePreview) , _data(data) -, _sponsoredData([&]() -> std::optional { - if (!(flags & MediaWebPageFlag::Sponsored)) { - return std::nullopt; - } - const auto &session = _parent->data()->history()->session(); - const auto details = session.sponsoredMessages().lookupDetails( - _parent->data()->fullId()); - auto result = std::make_optional(); - result->buttonText = details.buttonText; - result->isLinkInternal = details.isLinkInternal; - result->backgroundEmojiId = details.backgroundEmojiId; - result->colorIndex = details.colorIndex; - result->canReport = details.canReport; - return result; -}()) +, _flags(flags) , _siteName(st::msgMinWidth - _st.padding.left() - _st.padding.right()) , _title(st::msgMinWidth - _st.padding.left() - _st.padding.right()) -, _description(st::msgMinWidth - _st.padding.left() - _st.padding.right()) -, _flags(flags) { +, _description(st::msgMinWidth - _st.padding.left() - _st.padding.right()) { history()->owner().registerWebPageView(_data, _parent); } +void WebPage::setupAdditionalData() { + if (_flags & MediaWebPageFlag::Sponsored) { + _additionalData = std::make_unique(SponsoredData()); + const auto raw = sponsoredData(); + const auto &session = _parent->data()->history()->session(); + const auto details = session.sponsoredMessages().lookupDetails( + _parent->data()->fullId()); + raw->buttonText = details.buttonText; + raw->isLinkInternal = details.isLinkInternal ? 1 : 0; + raw->backgroundEmojiId = details.backgroundEmojiId; + raw->colorIndex = details.colorIndex; + raw->canReport = details.canReport ? 1 : 0; + } else if (_data->stickerSet) { + _additionalData = std::make_unique(StickerSetData()); + const auto raw = stickerSetData(); + for (const auto &sticker : _data->stickerSet->items) { + if (!sticker->sticker()) { + continue; + } + raw->views.push_back( + std::make_unique(_parent, sticker, true)); + } + const auto side = std::ceil(std::sqrt(raw->views.size())); + const auto box = UnitedLineHeight() * kStickerSetLines; + const auto single = box / side; + for (const auto &view : raw->views) { + view->setWebpagePart(); + view->initSize(single); + } + } else if (_data->type == WebPageType::Factcheck) { + _additionalData = std::make_unique(FactcheckData()); + } +} + QSize WebPage::countOptimalSize() { if (_data->pendingTill || _data->failed) { return { 0, 0 }; } + setupAdditionalData(); + + const auto sponsored = sponsoredData(); + const auto factcheck = factcheckData(); // Detect _openButtonWidth before counting paddings. _openButton = Ui::Text::String(); @@ -274,12 +335,10 @@ QSize WebPage::countOptimalSize() { PageToPhrase(_data), kMarkupTextOptions, context); - } else if (_sponsoredData) { - if (!_sponsoredData->buttonText.isEmpty()) { - _openButton.setText( - st::semiboldTextStyle, - Ui::Text::Upper(_sponsoredData->buttonText)); - } + } else if (sponsored && !sponsored->buttonText.isEmpty()) { + _openButton.setText( + st::semiboldTextStyle, + Ui::Text::Upper(sponsored->buttonText)); } const auto padding = inBubblePadding() + innerMargin(); @@ -296,25 +355,7 @@ QSize WebPage::countOptimalSize() { } const auto lineHeight = UnitedLineHeight(); - if (_data->stickerSet && !_stickerSet) { - _stickerSet = std::make_unique(); - for (const auto &sticker : _data->stickerSet->items) { - if (!sticker->sticker()) { - continue; - } - _stickerSet->views.push_back( - std::make_unique(_parent, sticker, true)); - } - const auto side = std::ceil(std::sqrt(_stickerSet->views.size())); - const auto box = lineHeight * kStickerSetLines; - const auto single = box / side; - for (const auto &view : _stickerSet->views) { - view->setWebpagePart(); - view->initSize(single); - } - } - - if (!_openl && (!_data->url.isEmpty() || _sponsoredData)) { + if (!_openl && (!_data->url.isEmpty() || sponsored || factcheck)) { const auto original = _parent->data()->originalText(); const auto previewOfHiddenUrl = [&] { if (_data->type == WebPageType::BotApp) { @@ -352,28 +393,31 @@ QSize WebPage::countOptimalSize() { } return true; }(); - _openl = _data->iv - ? IvClickHandler(_data, original) - : (previewOfHiddenUrl || UrlClickHandler::IsSuspicious( - _data->url)) - ? std::make_shared(_data->url) - : std::make_shared(_data->url, true); - if (_data->document + if (sponsored) { + _openl = SponsoredLink(_data->url, sponsored->isLinkInternal); + if (sponsored->canReport) { + sponsored->hint.link = AboutSponsoredClickHandler(); + } + } else if (factcheck) { + const auto item = _parent->data(); + if (const auto info = item->Get()) { + const auto country = info->data.country; + factcheck->hint.link = AboutFactcheckClickHandler(country); + } + } else if (_data->document && (_data->document->isWallPaper() || _data->document->isTheme())) { _openl = std::make_shared( std::move(_openl), _data->document, _parent->data()->fullId()); - } - if (_sponsoredData) { - _openl = SponsoredLink( - _data->url, - _sponsoredData->isLinkInternal); - - if (_sponsoredData->canReport) { - _sponsoredData->hintLink = AboutSponsoredClickHandler(); - } + } else { + _openl = _data->iv + ? IvClickHandler(_data, original) + : (previewOfHiddenUrl || UrlClickHandler::IsSuspicious( + _data->url)) + ? std::make_shared(_data->url) + : std::make_shared(_data->url, true); } } @@ -456,7 +500,12 @@ QSize WebPage::countOptimalSize() { const auto siteNameHeight = _siteName.isEmpty() ? 0 : lineHeight; const auto titleMinHeight = _title.isEmpty() ? 0 : lineHeight; - const auto descMaxLines = isLogEntryOriginal() + const auto factcheckMetrics = factcheck + ? computeFactcheckMetrics(_description.minHeight()) + : FactcheckMetrics(); + const auto descMaxLines = factcheck + ? factcheckMetrics.lines + : isLogEntryOriginal() ? kMaxOriginalEntryLines : (3 + (siteNameHeight ? 0 : 1) + (titleMinHeight ? 0 : 1)); const auto descriptionMinHeight = _description.isEmpty() @@ -517,15 +566,16 @@ QSize WebPage::countOptimalSize() { if (_asArticle) { minHeight = resizeGetHeight(maxWidth); } - if (_sponsoredData && _sponsoredData->canReport) { - _sponsoredData->widthBeforeHint - = st::webPageTitleStyle.font->width(siteName); + if (const auto hint = hintData()) { + hint->widthBefore = st::webPageTitleStyle.font->width(siteName); const auto &font = st::webPageSponsoredHintFont; - _sponsoredData->hintSize = QSize( - font->width(tr::lng_sponsored_message_revenue_button(tr::now)) - + font->height, + hint->text = sponsored + ? tr::lng_sponsored_message_revenue_button(tr::now) + : tr::lng_factcheck_whats_this(tr::now); + hint->size = QSize( + font->width(hint->text) + font->height, font->height); - maxWidth += _sponsoredData->hintSize.width(); + maxWidth += hint->size.width(); } return { maxWidth, minHeight }; } @@ -539,9 +589,22 @@ QSize WebPage::countCurrentSize(int newWidth) { const auto innerWidth = newWidth - rect::m::sum::h(padding); auto newHeight = 0; - const auto specialRightPix = (_sponsoredData || _stickerSet); + const auto stickerSet = stickerSetData(); + const auto factcheck = factcheckData(); + const auto specialRightPix = (sponsoredData() || stickerSet); const auto lineHeight = UnitedLineHeight(); - const auto linesMax = (specialRightPix || isLogEntryOriginal()) + const auto factcheckMetrics = factcheck + ? computeFactcheckMetrics(_description.countHeight(innerWidth)) + : FactcheckMetrics(); + if (factcheck) { + factcheck->expandable = factcheckMetrics.expandable; + _openl = factcheck->expandable + ? ToggleFactcheckClickHandler(_parent) + : nullptr; + } + const auto linesMax = factcheck + ? (factcheckMetrics.lines + 1) + : (specialRightPix || isLogEntryOriginal()) ? kMaxOriginalEntryLines : 5; const auto siteNameHeight = _siteNameLines ? lineHeight : 0; @@ -550,7 +613,7 @@ QSize WebPage::countCurrentSize(int newWidth) { if (asArticle() || specialRightPix) { constexpr auto kSponsoredUserpicLines = 2; _pixh = lineHeight - * (_stickerSet + * (stickerSet ? kStickerSetLines : specialRightPix ? kSponsoredUserpicLines @@ -673,8 +736,14 @@ void WebPage::ensurePhotoMediaCreated() const { } bool WebPage::hasHeavyPart() const { + if (const auto stickerSet = stickerSetData()) { + for (const auto &part : stickerSet->views) { + if (part->hasHeavyPart()) { + return true; + } + } + } return _photoMedia - || (_stickerSet) || (_attach ? _attach->hasHeavyPart() : false); } @@ -684,6 +753,11 @@ void WebPage::unloadHeavyPart() { } _description.unloadPersistentAnimation(); _photoMedia = nullptr; + if (const auto stickerSet = stickerSetData()) { + for (const auto &part : stickerSet->views) { + part->unloadHeavyPart(); + } + } } void WebPage::draw(Painter &p, const PaintContext &context) const { @@ -704,22 +778,22 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { auto tshift = inner.top(); auto paintw = inner.width(); - const auto asSponsored = (!!_sponsoredData); + const auto sponsored = sponsoredData(); const auto selected = context.selected(); const auto view = parent(); const auto from = view->data()->contentColorsFrom(); - const auto colorIndex = (asSponsored && _sponsoredData->colorIndex) - ? _sponsoredData->colorIndex + const auto colorIndex = (sponsored && sponsored->colorIndex) + ? sponsored->colorIndex : from ? from->colorIndex() : view->colorIndex(); const auto cache = context.outbg ? stm->replyCache[st->colorPatternIndex(colorIndex)].get() : st->coloredReplyCache(selected, colorIndex).get(); - const auto backgroundEmojiId = (asSponsored - && _sponsoredData->backgroundEmojiId) - ? _sponsoredData->backgroundEmojiId + const auto backgroundEmojiId = (sponsored + && sponsored->backgroundEmojiId) + ? sponsored->backgroundEmojiId : from ? from->backgroundEmojiId() : DocumentId(); @@ -755,8 +829,8 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { } auto lineHeight = UnitedLineHeight(); - if (_stickerSet) { - const auto viewsCount = _stickerSet->views.size(); + if (const auto stickerSet = stickerSetData()) { + const auto viewsCount = stickerSet->views.size(); const auto box = _pixh; const auto topLeft = QPoint(inner.left() + paintw - box, tshift); const auto side = std::ceil(std::sqrt(viewsCount)); @@ -767,7 +841,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { if (viewsCount <= index) { break; } - const auto &view = _stickerSet->views[index]; + const auto &view = stickerSet->views[index]; const auto size = view->countOptimalSize(); const auto offsetX = (single - size.width()) / 2.; const auto offsetY = (single - size.height()) / 2.; @@ -822,7 +896,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { st->msgSelectOverlay(), st->msgSelectOverlayCorners(Ui::CachedCornerRadius::Small)); } - if (!asSponsored) { + if (!sponsored) { // Ignore photo width in sponsored messages, // as its width only affects the title. paintw -= pw + st::webPagePhotoDelta; @@ -850,35 +924,31 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { endskip, false, context.selection); - if (asSponsored - && _sponsoredData->canReport - && (paintw > - _sponsoredData->widthBeforeHint - + _sponsoredData->hintSize.width())) { - if (_sponsoredData->hintRipple) { - _sponsoredData->hintRipple->paint( - p, - _sponsoredData->lastHintPos.x(), - _sponsoredData->lastHintPos.y(), - width(), - &cache->bg); - if (_sponsoredData->hintRipple->empty()) { - _sponsoredData->hintRipple = nullptr; - } - } - + const auto hint = hintData(); + if (hint && (paintw > hint->widthBefore + hint->size.width())) { auto color = cache->icon; color.setAlphaF(color.alphaF() * 0.15); const auto height = st::webPageSponsoredHintFont->height; const auto radius = height / 2; - _sponsoredData->lastHintPos = QPointF( - radius + inner.left() + _sponsoredData->widthBeforeHint, + hint->lastPosition = QPointF( + radius + inner.left() + hint->widthBefore, tshift + (_siteName.style()->font->height - height) / 2.); - const auto rect = QRectF( - _sponsoredData->lastHintPos, - _sponsoredData->hintSize); + + if (hint->ripple) { + hint->ripple->paint( + p, + hint->lastPosition.x(), + hint->lastPosition.y(), + width(), + &cache->bg); + if (hint->ripple->empty()) { + hint->ripple = nullptr; + } + } + + const auto rect = QRectF(hint->lastPosition, hint->size); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(color); @@ -887,10 +957,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { p.setPen(cache->icon); p.setBrush(Qt::NoBrush); p.setFont(st::webPageSponsoredHintFont); - p.drawText( - rect, - tr::lng_sponsored_message_revenue_button(tr::now), - style::al_center); + p.drawText(rect, hint->text, style::al_center); } tshift += lineHeight; @@ -901,7 +968,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { const auto endskip = _title.hasSkipBlock() ? _parent->skipBlockWidth() : 0; - const auto titleWidth = asSponsored + const auto titleWidth = sponsored ? (paintw - _pixh - st::webPagePhotoDelta) : paintw; _title.drawLeftElided( @@ -1051,16 +1118,38 @@ bool WebPage::asArticle() const { return _asArticle && (_data->photo != nullptr); } +WebPage::StickerSetData *WebPage::stickerSetData() const { + return std::get_if(_additionalData.get()); +} + +WebPage::SponsoredData *WebPage::sponsoredData() const { + return std::get_if(_additionalData.get()); +} + +WebPage::FactcheckData *WebPage::factcheckData() const { + return std::get_if(_additionalData.get()); +} + +WebPage::HintData *WebPage::hintData() const { + if (const auto sponsored = sponsoredData()) { + return sponsored->hint.link ? &sponsored->hint : nullptr; + } else if (const auto factcheck = factcheckData()) { + return factcheck->hint.link ? &factcheck->hint : nullptr; + } + return nullptr; +} + TextState WebPage::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); if (width() < rect::m::sum::h(st::msgPadding) + 1) { return result; } + const auto sponsored = sponsoredData(); const auto bubble = _attach ? _attach->bubbleMargins() : QMargins(); const auto full = Rect(currentSize()); auto outer = full - inBubblePadding(); - if (_sponsoredData) { + if (sponsored) { outer.translate(0, st::msgDateFont->height); } const auto inner = outer - innerMargin(); @@ -1175,16 +1264,15 @@ TextState WebPage::textState(QPoint point, StateRequest request) const { } } } - if ((!result.link || _sponsoredData) && outer.contains(point)) { + if ((!result.link || sponsored) && outer.contains(point)) { result.link = _openl; } - if (_sponsoredData && _sponsoredData->canReport) { - const auto contains = QRectF( - _sponsoredData->lastHintPos, - _sponsoredData->hintSize).contains(point - - QPoint(0, st::msgDateFont->height)); - if (contains) { - result.link = _sponsoredData->hintLink; + if (const auto hint = hintData()) { + const auto check = point + - QPoint(0, sponsored ? st::msgDateFont->height : 0); + const auto hintRect = QRectF(hint->lastPosition, hint->size); + if (hintRect.contains(check)) { + result.link = hint->link; } } _lastPoint = point - outer.topLeft(); @@ -1256,25 +1344,25 @@ void WebPage::clickHandlerActiveChanged( void WebPage::clickHandlerPressedChanged( const ClickHandlerPtr &p, bool pressed) { - if (_sponsoredData && _sponsoredData->hintLink == p) { + const auto hint = hintData(); + if (hint && hint->link == p) { if (pressed) { - if (!_sponsoredData->hintRipple) { + if (!hint->ripple) { const auto owner = &parent()->history()->owner(); - auto ripple = std::make_unique( + hint->ripple = std::make_unique( st::defaultRippleAnimation, Ui::RippleAnimation::RoundRectMask( - _sponsoredData->hintSize, + hint->size, _st.radius), [=] { owner->requestViewRepaint(parent()); }); - _sponsoredData->hintRipple = std::move(ripple); } const auto full = Rect(currentSize()); const auto outer = full - inBubblePadding(); - _sponsoredData->hintRipple->add(_lastPoint + hint->ripple->add(_lastPoint + outer.topLeft() - - _sponsoredData->lastHintPos.toPoint()); - } else if (_sponsoredData->hintRipple) { - _sponsoredData->hintRipple->lastStop(); + - hint->lastPosition.toPoint()); + } else if (hint->ripple) { + hint->ripple->lastStop(); } return; } @@ -1387,6 +1475,19 @@ bool WebPage::isLogEntryOriginal() const { return _parent->data()->isAdminLogEntry() && _parent->media() != this; } +WebPage::FactcheckMetrics WebPage::computeFactcheckMetrics( + int fullHeight) const { + const auto possible = fullHeight / st::normalFont->height; + const auto expandable = (possible > kFactcheckCollapsedLines + 1); + const auto check = _parent->Get(); + const auto expanded = check && check->expanded; + const auto allowExpanding = (expanded || !expandable); + return { + .lines = allowExpanding ? possible : kFactcheckCollapsedLines, + .expandable = expandable, + }; +} + int WebPage::bottomInfoPadding() const { if (!isBubbleBottom()) { return 0; diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.h b/Telegram/SourceFiles/history/view/media/history_view_web_page.h index 86f7ec817..47cba266c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.h +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.h @@ -101,6 +101,40 @@ public: ~WebPage(); private: + struct FactcheckMetrics { + int lines = 0; + bool expandable = false; + }; + struct HintData { + QSize size; + QPointF lastPosition; + QString text; + int widthBefore = 0; + std::unique_ptr ripple; + ClickHandlerPtr link; + }; + struct StickerSetData { + std::vector> views; + }; + struct SponsoredData { + QString buttonText; + + uint64 backgroundEmojiId = 0; + uint8 colorIndex : 6 = 0; + uint8 isLinkInternal : 1 = 0; + uint8 canReport : 1 = 0; + + HintData hint; + }; + struct FactcheckData { + HintData hint; + bool expandable = false; + }; + using AdditionalData = std::variant< + StickerSetData, + SponsoredData, + FactcheckData>; + void playAnimation(bool autoplay) override; QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; @@ -124,36 +158,26 @@ private: const ClickHandlerPtr &link) const; [[nodiscard]] bool asArticle() const; + [[nodiscard]] StickerSetData *stickerSetData() const; + [[nodiscard]] SponsoredData *sponsoredData() const; + [[nodiscard]] FactcheckData *factcheckData() const; + [[nodiscard]] HintData *hintData() const; + + [[nodiscard]] FactcheckMetrics computeFactcheckMetrics( + int fullHeight) const; + + void setupAdditionalData(); + const style::QuoteStyle &_st; const not_null _data; + const MediaWebPageFlags _flags; + std::vector> _collage; ClickHandlerPtr _openl; std::unique_ptr _attach; mutable std::shared_ptr _photoMedia; mutable std::unique_ptr _ripple; - struct StickerSet final { - std::vector> views; - }; - - std::unique_ptr _stickerSet; - - struct SponsoredData final { - QString buttonText; - bool isLinkInternal = false; - - uint64 backgroundEmojiId = 0; - uint8 colorIndex : 6 = 0; - - bool canReport = false; - QSize hintSize; - QPointF lastHintPos; - int widthBeforeHint = 0; - std::unique_ptr hintRipple; - ClickHandlerPtr hintLink; - }; - mutable std::optional _sponsoredData; - int _dataVersion = -1; int _siteNameLines = 0; int _descriptionLines = 0; @@ -172,7 +196,7 @@ private: int _pixw = 0; int _pixh = 0; - const MediaWebPageFlags _flags; + std::unique_ptr _additionalData; };