From f2e4a5a35a7522fe6d8345e8816c3d36336bb5a4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 18 Nov 2021 16:03:12 +0400 Subject: [PATCH] Highlight YouTube video timestamps as external links. --- .../SourceFiles/core/click_handler_types.cpp | 20 ++++++ .../SourceFiles/core/click_handler_types.h | 8 +-- .../SourceFiles/core/local_url_handlers.cpp | 13 ++++ .../history/history_item_components.cpp | 10 ++- .../history/history_item_components.h | 4 +- .../SourceFiles/history/history_message.cpp | 51 +++++++++----- .../view/media/history_view_document.cpp | 7 +- .../history/view/media/history_view_gif.cpp | 6 +- .../history/view/media/history_view_media.cpp | 69 +++++++++++++++++-- .../history/view/media/history_view_media.h | 11 ++- .../view/media/history_view_media_grouped.cpp | 14 ++-- .../media/view/media_view_overlay_widget.cpp | 10 +-- 12 files changed, 170 insertions(+), 53 deletions(-) diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 74a9009e9..151b07085 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -65,6 +65,26 @@ bool UrlRequiresConfirmation(const QUrl &url) { RegExOption::CaseInsensitive); } +QString HiddenUrlClickHandler::copyToClipboardText() const { + return url().startsWith(qstr("internal:url:")) + ? url().mid(qstr("internal:url:").size()) + : url(); +} + +QString HiddenUrlClickHandler::copyToClipboardContextItemText() const { + return url().isEmpty() + ? QString() + : !url().startsWith(qstr("internal:")) + ? UrlClickHandler::copyToClipboardContextItemText() + : url().startsWith(qstr("internal:url:")) + ? UrlClickHandler::copyToClipboardContextItemText() + : QString(); +} + +QString HiddenUrlClickHandler::dragText() const { + return HiddenUrlClickHandler::copyToClipboardText(); +} + void HiddenUrlClickHandler::Open(QString url, QVariant context) { url = Core::TryConvertUrlToLocal(url); if (Core::InternalPassportLink(url)) { diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index a4ff0f8b1..b68d212e4 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -39,11 +39,9 @@ class HiddenUrlClickHandler : public UrlClickHandler { public: HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) { } - QString copyToClipboardContextItemText() const override { - return (url().isEmpty() || url().startsWith(qstr("internal:"))) - ? QString() - : UrlClickHandler::copyToClipboardContextItemText(); - } + QString copyToClipboardText() const override; + QString copyToClipboardContextItemText() const override; + QString dragText() const override; static void Open(QString url, QVariant context = {}); void onClick(ClickContext context) const override { diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index f4c7c5b8f..e45295f4e 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -474,6 +474,15 @@ bool ShowInviteLink( return true; } +bool OpenExternalLink( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + return Ui::Integration::Instance().handleUrlClick( + match->captured(1), + context); +} + void ExportTestChatTheme( not_null session, not_null theme) { @@ -698,6 +707,10 @@ const std::vector &InternalUrlHandlers() { qsl("^show_invite_link/?\\?link=([a-zA-Z0-9_\\+\\/\\=\\-]+)(&|$)"), ShowInviteLink }, + { + qsl("^url:(.+)$"), + OpenExternalLink + }, }; return Result; } diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 50e8818c5..9f1327872 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_file_origin.h" #include "data/data_document.h" +#include "data/data_web_page.h" #include "data/data_file_click_handler.h" #include "main/main_session.h" #include "window/window_session_controller.h" @@ -224,7 +225,7 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { bool HistoryMessageReply::updateData( not_null holder, bool force) { - const auto guard = gsl::finally([&] { refreshReplyToDocument(); }); + const auto guard = gsl::finally([&] { refreshReplyToMedia(); }); if (!force) { if (replyToMsg || !replyToMsgId) { return true; @@ -291,7 +292,7 @@ void HistoryMessageReply::clearData(not_null holder) { replyToMsg = nullptr; } replyToMsgId = 0; - refreshReplyToDocument(); + refreshReplyToMedia(); } bool HistoryMessageReply::isNameUpdated() const { @@ -416,11 +417,14 @@ void HistoryMessageReply::paint( } } -void HistoryMessageReply::refreshReplyToDocument() { +void HistoryMessageReply::refreshReplyToMedia() { replyToDocumentId = 0; + replyToWebPageId = 0; if (const auto media = replyToMsg ? replyToMsg->media() : nullptr) { if (const auto document = media->document()) { replyToDocumentId = document->id; + } else if (const auto webpage = media->webpage()) { + replyToWebPageId = webpage->id; } } } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index b41504da2..880b10639 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -130,6 +130,7 @@ struct HistoryMessageReply : public RuntimeComponent holder); - void refreshReplyToDocument(); + void refreshReplyToMedia(); PeerId replyToPeerId = 0; MsgId replyToMsgId = 0; MsgId replyToMsgTop = 0; HistoryItem *replyToMsg = nullptr; DocumentId replyToDocumentId = 0; + WebPageId replyToWebPageId = 0; ClickHandlerPtr replyToLnk; mutable Ui::Text::String replyToName, replyToText; mutable int replyToVersion = 0; diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index f6c1542c4..7ef0b990f 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_user.h" #include "data/data_histories.h" +#include "data/data_web_page.h" #include "styles/style_dialogs.h" #include "styles/style_widgets.h" #include "styles/style_chat.h" @@ -1000,9 +1001,11 @@ void HistoryMessage::setCommentsItemId(FullMsgId id) { bool HistoryMessage::updateDependencyItem() { if (const auto reply = Get()) { const auto documentId = reply->replyToDocumentId; + const auto webpageId = reply->replyToWebPageId; const auto result = reply->updateData(this, true); - if (documentId != reply->replyToDocumentId - && generateLocalEntitiesByReply()) { + const auto mediaIdChanged = (documentId != reply->replyToDocumentId) + || (webpageId != reply->replyToWebPageId); + if (mediaIdChanged && generateLocalEntitiesByReply()) { reapplyText(); } return result; @@ -1524,34 +1527,50 @@ Storage::SharedMediaTypesMask HistoryMessage::sharedMediaTypes() const { } bool HistoryMessage::generateLocalEntitiesByReply() const { - return !_media || _media->webpage(); + if (!_media) { + return true; + } else if (const auto webpage = _media->webpage()) { + return !webpage->document && webpage->type != WebPageType::Video; + } + return false; } TextWithEntities HistoryMessage::withLocalEntities( const TextWithEntities &textWithEntities) const { + using namespace HistoryView; if (!generateLocalEntitiesByReply()) { + if (const auto webpage = _media ? _media->webpage() : nullptr) { + if (const auto duration = DurationForTimestampLinks(webpage)) { + return AddTimestampLinks( + textWithEntities, + duration, + TimestampLinkBase(webpage, fullId())); + } + } return textWithEntities; } if (const auto reply = Get()) { const auto document = reply->replyToDocumentId ? history()->owner().document(reply->replyToDocumentId).get() : nullptr; - if (document - && (document->isVideoFile() - || document->isSong() - || document->isVoiceMessage())) { - using namespace HistoryView; - const auto duration = document->getDuration(); - const auto base = (duration > 0) - ? DocumentTimestampLinkBase( - document, - reply->replyToMsg->fullId()) - : QString(); - if (!base.isEmpty()) { + const auto webpage = reply->replyToWebPageId + ? history()->owner().webpage(reply->replyToWebPageId).get() + : nullptr; + if (document) { + if (const auto duration = DurationForTimestampLinks(document)) { + const auto context = reply->replyToMsg->fullId(); return AddTimestampLinks( textWithEntities, duration, - base); + TimestampLinkBase(document, context)); + } + } else if (webpage) { + if (const auto duration = DurationForTimestampLinks(webpage)) { + const auto context = reply->replyToMsg->fullId(); + return AddTimestampLinks( + textWithEntities, + duration, + TimestampLinkBase(webpage, context)); } } } diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index c5244fd01..502e7b487 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -1084,12 +1084,9 @@ TextWithEntities Document::getCaption() const { } Ui::Text::String Document::createCaption() { - const auto timestampLinksDuration = (_data->isSong() - || _data->isVoiceMessage()) - ? _data->getDuration() - : 0; + const auto timestampLinksDuration = DurationForTimestampLinks(_data); const auto timestampLinkBase = timestampLinksDuration - ? DocumentTimestampLinkBase(_data, _realParent->fullId()) + ? TimestampLinkBase(_data, _realParent->fullId()) : QString(); return File::createCaption( _realParent, diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 1e025b7fd..75772a124 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -1322,11 +1322,9 @@ void Gif::refreshParentId(not_null realParent) { } void Gif::refreshCaption() { - const auto timestampLinksDuration = _data->isVideoFile() - ? _data->getDuration() - : 0; + const auto timestampLinksDuration = DurationForTimestampLinks(_data); const auto timestampLinkBase = timestampLinksDuration - ? DocumentTimestampLinkBase(_data, _realParent->fullId()) + ? TimestampLinkBase(_data, _realParent->fullId()) : QString(); _caption = createCaption( _parent->data(), diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index 09d24d454..5e3cebf77 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lottie/lottie_single_player.h" #include "storage/storage_shared_media.h" #include "data/data_document.h" +#include "data/data_web_page.h" #include "ui/item_text_options.h" #include "ui/chat/chat_style.h" #include "ui/chat/message_bubble.h" @@ -44,18 +45,77 @@ namespace { } // namespace -QString DocumentTimestampLinkBase( +TimeId DurationForTimestampLinks(not_null document) { + if (!document->isVideoFile() + && !document->isSong() + && !document->isVoiceMessage()) { + return TimeId(0); + } + return std::max(document->getDuration(), TimeId(0)); +} + +QString TimestampLinkBase( not_null document, FullMsgId context) { return QString( - "doc%1_%2_%3" + "media_timestamp?base=doc%1_%2_%3&t=" ).arg(document->id).arg(context.channel.bare).arg(context.msg.bare); } +TimeId DurationForTimestampLinks(not_null webpage) { + if (!webpage->collage.items.empty()) { + return false; + } else if (const auto document = webpage->document) { + return DurationForTimestampLinks(document); + } else if (webpage->type != WebPageType::Video + || webpage->siteName != qstr("YouTube")) { + return TimeId(0); + } else if (webpage->duration > 0) { + return webpage->duration; + } + constexpr auto kMaxYouTubeTimestampDuration = 10 * 60 * TimeId(60); + return kMaxYouTubeTimestampDuration; +} + +QString TimestampLinkBase( + not_null webpage, + FullMsgId context) { + const auto url = webpage->url; + if (url.isEmpty()) { + return QString(); + } + auto parts = url.split(QChar('#')); + const auto base = parts[0]; + parts.pop_front(); + const auto use = [&] { + const auto query = base.indexOf(QChar('?')); + if (query < 0) { + return base + QChar('?'); + } + auto params = base.mid(query + 1).split(QChar('&')); + for (auto i = params.begin(); i != params.end();) { + if (i->startsWith("t=")) { + i = params.erase(i); + } else { + ++i; + } + } + return base.mid(0, query) + + (params.empty() ? "?" : ("?" + params.join(QChar('&')) + "&")); + }(); + return "url:" + + use + + "t=" + + (parts.empty() ? QString() : ("#" + parts.join(QChar('#')))); +} + TextWithEntities AddTimestampLinks( TextWithEntities text, TimeId duration, const QString &base) { + if (base.isEmpty()) { + return text; + } static const auto expression = QRegularExpression( "(? document); +[[nodiscard]] QString TimestampLinkBase( not_null document, FullMsgId context); + +[[nodiscard]] TimeId DurationForTimestampLinks( + not_null webpage); +[[nodiscard]] QString TimestampLinkBase( + not_null webpage, + FullMsgId context); + [[nodiscard]] TextWithEntities AddTimestampLinks( TextWithEntities text, TimeId duration, diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp index a2992af1a..aa26f92f2 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -677,18 +677,16 @@ void GroupedMedia::updateNeedBubbleState() { QString base; }; const auto timestamp = [&]() -> Timestamp { - const auto &document = part->content->getDocument(); - if (!document || document->isAnimation()) { + const auto document = part->content->getDocument(); + const auto duration = document + ? DurationForTimestampLinks(document) + : TimeId(0); + if (!duration) { return {}; } - const auto duration = document->getDuration(); return { .duration = duration, - .base = duration - ? DocumentTimestampLinkBase( - document, - part->item->fullId()) - : QString(), + .base = TimestampLinkBase(document, part->item->fullId()), }; }(); _caption = createCaption( diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index d3d1d9900..6618b4cc8 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -2102,18 +2102,20 @@ void OverlayWidget::refreshCaption() { using namespace HistoryView; _caption = Ui::Text::String(st::msgMinWidth); - const auto duration = (_streamed && _document && !videoIsGifOrUserpic()) - ? _document->getDuration() + const auto duration = (_streamed && _document) + ? DurationForTimestampLinks(_document) : 0; const auto base = duration - ? DocumentTimestampLinkBase(_document, _message->fullId()) + ? TimestampLinkBase(_document, _message->fullId()) : QString(); const auto context = Core::MarkedTextContext{ .session = &_message->history()->session() }; _caption.setMarkedText( st::mediaviewCaptionStyle, - AddTimestampLinks(caption, duration, base), + (base.isEmpty() + ? caption + : AddTimestampLinks(caption, duration, base)), Ui::ItemTextOptions(_message), context); }