Highlight YouTube video timestamps as external links.

This commit is contained in:
John Preston 2021-11-18 16:03:12 +04:00
parent ebded1b421
commit f2e4a5a35a
12 changed files with 170 additions and 53 deletions

View File

@ -65,6 +65,26 @@ bool UrlRequiresConfirmation(const QUrl &url) {
RegExOption::CaseInsensitive); 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) { void HiddenUrlClickHandler::Open(QString url, QVariant context) {
url = Core::TryConvertUrlToLocal(url); url = Core::TryConvertUrlToLocal(url);
if (Core::InternalPassportLink(url)) { if (Core::InternalPassportLink(url)) {

View File

@ -39,11 +39,9 @@ class HiddenUrlClickHandler : public UrlClickHandler {
public: public:
HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) { HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) {
} }
QString copyToClipboardContextItemText() const override { QString copyToClipboardText() const override;
return (url().isEmpty() || url().startsWith(qstr("internal:"))) QString copyToClipboardContextItemText() const override;
? QString() QString dragText() const override;
: UrlClickHandler::copyToClipboardContextItemText();
}
static void Open(QString url, QVariant context = {}); static void Open(QString url, QVariant context = {});
void onClick(ClickContext context) const override { void onClick(ClickContext context) const override {

View File

@ -474,6 +474,15 @@ bool ShowInviteLink(
return true; return true;
} }
bool OpenExternalLink(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
return Ui::Integration::Instance().handleUrlClick(
match->captured(1),
context);
}
void ExportTestChatTheme( void ExportTestChatTheme(
not_null<Main::Session*> session, not_null<Main::Session*> session,
not_null<const Data::CloudTheme*> theme) { not_null<const Data::CloudTheme*> theme) {
@ -698,6 +707,10 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
qsl("^show_invite_link/?\\?link=([a-zA-Z0-9_\\+\\/\\=\\-]+)(&|$)"), qsl("^show_invite_link/?\\?link=([a-zA-Z0-9_\\+\\/\\=\\-]+)(&|$)"),
ShowInviteLink ShowInviteLink
}, },
{
qsl("^url:(.+)$"),
OpenExternalLink
},
}; };
return Result; return Result;
} }

View File

@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h" #include "data/data_user.h"
#include "data/data_file_origin.h" #include "data/data_file_origin.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_web_page.h"
#include "data/data_file_click_handler.h" #include "data/data_file_click_handler.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
@ -224,7 +225,7 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const {
bool HistoryMessageReply::updateData( bool HistoryMessageReply::updateData(
not_null<HistoryMessage*> holder, not_null<HistoryMessage*> holder,
bool force) { bool force) {
const auto guard = gsl::finally([&] { refreshReplyToDocument(); }); const auto guard = gsl::finally([&] { refreshReplyToMedia(); });
if (!force) { if (!force) {
if (replyToMsg || !replyToMsgId) { if (replyToMsg || !replyToMsgId) {
return true; return true;
@ -291,7 +292,7 @@ void HistoryMessageReply::clearData(not_null<HistoryMessage*> holder) {
replyToMsg = nullptr; replyToMsg = nullptr;
} }
replyToMsgId = 0; replyToMsgId = 0;
refreshReplyToDocument(); refreshReplyToMedia();
} }
bool HistoryMessageReply::isNameUpdated() const { bool HistoryMessageReply::isNameUpdated() const {
@ -416,11 +417,14 @@ void HistoryMessageReply::paint(
} }
} }
void HistoryMessageReply::refreshReplyToDocument() { void HistoryMessageReply::refreshReplyToMedia() {
replyToDocumentId = 0; replyToDocumentId = 0;
replyToWebPageId = 0;
if (const auto media = replyToMsg ? replyToMsg->media() : nullptr) { if (const auto media = replyToMsg ? replyToMsg->media() : nullptr) {
if (const auto document = media->document()) { if (const auto document = media->document()) {
replyToDocumentId = document->id; replyToDocumentId = document->id;
} else if (const auto webpage = media->webpage()) {
replyToWebPageId = webpage->id;
} }
} }
} }

View File

@ -130,6 +130,7 @@ struct HistoryMessageReply : public RuntimeComponent<HistoryMessageReply, Histor
replyToMsgId = other.replyToMsgId; replyToMsgId = other.replyToMsgId;
replyToMsgTop = other.replyToMsgTop; replyToMsgTop = other.replyToMsgTop;
replyToDocumentId = other.replyToDocumentId; replyToDocumentId = other.replyToDocumentId;
replyToWebPageId = other.replyToWebPageId;
std::swap(replyToMsg, other.replyToMsg); std::swap(replyToMsg, other.replyToMsg);
replyToLnk = std::move(other.replyToLnk); replyToLnk = std::move(other.replyToLnk);
replyToName = std::move(other.replyToName); replyToName = std::move(other.replyToName);
@ -182,13 +183,14 @@ struct HistoryMessageReply : public RuntimeComponent<HistoryMessageReply, Histor
void setReplyToLinkFrom( void setReplyToLinkFrom(
not_null<HistoryMessage*> holder); not_null<HistoryMessage*> holder);
void refreshReplyToDocument(); void refreshReplyToMedia();
PeerId replyToPeerId = 0; PeerId replyToPeerId = 0;
MsgId replyToMsgId = 0; MsgId replyToMsgId = 0;
MsgId replyToMsgTop = 0; MsgId replyToMsgTop = 0;
HistoryItem *replyToMsg = nullptr; HistoryItem *replyToMsg = nullptr;
DocumentId replyToDocumentId = 0; DocumentId replyToDocumentId = 0;
WebPageId replyToWebPageId = 0;
ClickHandlerPtr replyToLnk; ClickHandlerPtr replyToLnk;
mutable Ui::Text::String replyToName, replyToText; mutable Ui::Text::String replyToName, replyToText;
mutable int replyToVersion = 0; mutable int replyToVersion = 0;

View File

@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "data/data_histories.h" #include "data/data_histories.h"
#include "data/data_web_page.h"
#include "styles/style_dialogs.h" #include "styles/style_dialogs.h"
#include "styles/style_widgets.h" #include "styles/style_widgets.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
@ -1000,9 +1001,11 @@ void HistoryMessage::setCommentsItemId(FullMsgId id) {
bool HistoryMessage::updateDependencyItem() { bool HistoryMessage::updateDependencyItem() {
if (const auto reply = Get<HistoryMessageReply>()) { if (const auto reply = Get<HistoryMessageReply>()) {
const auto documentId = reply->replyToDocumentId; const auto documentId = reply->replyToDocumentId;
const auto webpageId = reply->replyToWebPageId;
const auto result = reply->updateData(this, true); const auto result = reply->updateData(this, true);
if (documentId != reply->replyToDocumentId const auto mediaIdChanged = (documentId != reply->replyToDocumentId)
&& generateLocalEntitiesByReply()) { || (webpageId != reply->replyToWebPageId);
if (mediaIdChanged && generateLocalEntitiesByReply()) {
reapplyText(); reapplyText();
} }
return result; return result;
@ -1524,34 +1527,50 @@ Storage::SharedMediaTypesMask HistoryMessage::sharedMediaTypes() const {
} }
bool HistoryMessage::generateLocalEntitiesByReply() 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( TextWithEntities HistoryMessage::withLocalEntities(
const TextWithEntities &textWithEntities) const { const TextWithEntities &textWithEntities) const {
using namespace HistoryView;
if (!generateLocalEntitiesByReply()) { 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; return textWithEntities;
} }
if (const auto reply = Get<HistoryMessageReply>()) { if (const auto reply = Get<HistoryMessageReply>()) {
const auto document = reply->replyToDocumentId const auto document = reply->replyToDocumentId
? history()->owner().document(reply->replyToDocumentId).get() ? history()->owner().document(reply->replyToDocumentId).get()
: nullptr; : nullptr;
if (document const auto webpage = reply->replyToWebPageId
&& (document->isVideoFile() ? history()->owner().webpage(reply->replyToWebPageId).get()
|| document->isSong() : nullptr;
|| document->isVoiceMessage())) { if (document) {
using namespace HistoryView; if (const auto duration = DurationForTimestampLinks(document)) {
const auto duration = document->getDuration(); const auto context = reply->replyToMsg->fullId();
const auto base = (duration > 0)
? DocumentTimestampLinkBase(
document,
reply->replyToMsg->fullId())
: QString();
if (!base.isEmpty()) {
return AddTimestampLinks( return AddTimestampLinks(
textWithEntities, textWithEntities,
duration, 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));
} }
} }
} }

View File

@ -1084,12 +1084,9 @@ TextWithEntities Document::getCaption() const {
} }
Ui::Text::String Document::createCaption() { Ui::Text::String Document::createCaption() {
const auto timestampLinksDuration = (_data->isSong() const auto timestampLinksDuration = DurationForTimestampLinks(_data);
|| _data->isVoiceMessage())
? _data->getDuration()
: 0;
const auto timestampLinkBase = timestampLinksDuration const auto timestampLinkBase = timestampLinksDuration
? DocumentTimestampLinkBase(_data, _realParent->fullId()) ? TimestampLinkBase(_data, _realParent->fullId())
: QString(); : QString();
return File::createCaption( return File::createCaption(
_realParent, _realParent,

View File

@ -1322,11 +1322,9 @@ void Gif::refreshParentId(not_null<HistoryItem*> realParent) {
} }
void Gif::refreshCaption() { void Gif::refreshCaption() {
const auto timestampLinksDuration = _data->isVideoFile() const auto timestampLinksDuration = DurationForTimestampLinks(_data);
? _data->getDuration()
: 0;
const auto timestampLinkBase = timestampLinksDuration const auto timestampLinkBase = timestampLinksDuration
? DocumentTimestampLinkBase(_data, _realParent->fullId()) ? TimestampLinkBase(_data, _realParent->fullId())
: QString(); : QString();
_caption = createCaption( _caption = createCaption(
_parent->data(), _parent->data(),

View File

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lottie/lottie_single_player.h" #include "lottie/lottie_single_player.h"
#include "storage/storage_shared_media.h" #include "storage/storage_shared_media.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_web_page.h"
#include "ui/item_text_options.h" #include "ui/item_text_options.h"
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
#include "ui/chat/message_bubble.h" #include "ui/chat/message_bubble.h"
@ -44,18 +45,77 @@ namespace {
} // namespace } // namespace
QString DocumentTimestampLinkBase( TimeId DurationForTimestampLinks(not_null<DocumentData*> document) {
if (!document->isVideoFile()
&& !document->isSong()
&& !document->isVoiceMessage()) {
return TimeId(0);
}
return std::max(document->getDuration(), TimeId(0));
}
QString TimestampLinkBase(
not_null<DocumentData*> document, not_null<DocumentData*> document,
FullMsgId context) { FullMsgId context) {
return QString( 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); ).arg(document->id).arg(context.channel.bare).arg(context.msg.bare);
} }
TimeId DurationForTimestampLinks(not_null<WebPageData*> 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<WebPageData*> 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 AddTimestampLinks(
TextWithEntities text, TextWithEntities text,
TimeId duration, TimeId duration,
const QString &base) { const QString &base) {
if (base.isEmpty()) {
return text;
}
static const auto expression = QRegularExpression( static const auto expression = QRegularExpression(
"(?<![^\\s\\(\\)\"\\,\\.\\-])(?:(?:(\\d{1,2}):)?(\\d))?(\\d):(\\d\\d)(?![^\\s\\(\\)\",\\.\\-])"); "(?<![^\\s\\(\\)\"\\,\\.\\-])(?:(?:(\\d{1,2}):)?(\\d))?(\\d):(\\d\\d)(?![^\\s\\(\\)\",\\.\\-])");
const auto &string = text.text; const auto &string = text.text;
@ -104,10 +164,7 @@ TextWithEntities AddTimestampLinks(
EntityType::CustomUrl, EntityType::CustomUrl,
from, from,
till - from, till - from,
("internal:media_timestamp?base=" ("internal:" + base + QString::number(time))));
+ base
+ "&t="
+ QString::number(time))));
} }
return text; return text;
} }

View File

@ -52,9 +52,18 @@ enum class MediaInBubbleState {
Bottom, Bottom,
}; };
[[nodiscard]] QString DocumentTimestampLinkBase( [[nodiscard]] TimeId DurationForTimestampLinks(
not_null<DocumentData*> document);
[[nodiscard]] QString TimestampLinkBase(
not_null<DocumentData*> document, not_null<DocumentData*> document,
FullMsgId context); FullMsgId context);
[[nodiscard]] TimeId DurationForTimestampLinks(
not_null<WebPageData*> webpage);
[[nodiscard]] QString TimestampLinkBase(
not_null<WebPageData*> webpage,
FullMsgId context);
[[nodiscard]] TextWithEntities AddTimestampLinks( [[nodiscard]] TextWithEntities AddTimestampLinks(
TextWithEntities text, TextWithEntities text,
TimeId duration, TimeId duration,

View File

@ -677,18 +677,16 @@ void GroupedMedia::updateNeedBubbleState() {
QString base; QString base;
}; };
const auto timestamp = [&]() -> Timestamp { const auto timestamp = [&]() -> Timestamp {
const auto &document = part->content->getDocument(); const auto document = part->content->getDocument();
if (!document || document->isAnimation()) { const auto duration = document
? DurationForTimestampLinks(document)
: TimeId(0);
if (!duration) {
return {}; return {};
} }
const auto duration = document->getDuration();
return { return {
.duration = duration, .duration = duration,
.base = duration .base = TimestampLinkBase(document, part->item->fullId()),
? DocumentTimestampLinkBase(
document,
part->item->fullId())
: QString(),
}; };
}(); }();
_caption = createCaption( _caption = createCaption(

View File

@ -2102,18 +2102,20 @@ void OverlayWidget::refreshCaption() {
using namespace HistoryView; using namespace HistoryView;
_caption = Ui::Text::String(st::msgMinWidth); _caption = Ui::Text::String(st::msgMinWidth);
const auto duration = (_streamed && _document && !videoIsGifOrUserpic()) const auto duration = (_streamed && _document)
? _document->getDuration() ? DurationForTimestampLinks(_document)
: 0; : 0;
const auto base = duration const auto base = duration
? DocumentTimestampLinkBase(_document, _message->fullId()) ? TimestampLinkBase(_document, _message->fullId())
: QString(); : QString();
const auto context = Core::MarkedTextContext{ const auto context = Core::MarkedTextContext{
.session = &_message->history()->session() .session = &_message->history()->session()
}; };
_caption.setMarkedText( _caption.setMarkedText(
st::mediaviewCaptionStyle, st::mediaviewCaptionStyle,
AddTimestampLinks(caption, duration, base), (base.isEmpty()
? caption
: AddTimestampLinks(caption, duration, base)),
Ui::ItemTextOptions(_message), Ui::ItemTextOptions(_message),
context); context);
} }