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);
}
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)) {

View File

@ -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 {

View File

@ -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<Main::Session*> session,
not_null<const Data::CloudTheme*> theme) {
@ -698,6 +707,10 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
qsl("^show_invite_link/?\\?link=([a-zA-Z0-9_\\+\\/\\=\\-]+)(&|$)"),
ShowInviteLink
},
{
qsl("^url:(.+)$"),
OpenExternalLink
},
};
return Result;
}

View File

@ -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<HistoryMessage*> 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<HistoryMessage*> 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;
}
}
}

View File

@ -130,6 +130,7 @@ struct HistoryMessageReply : public RuntimeComponent<HistoryMessageReply, Histor
replyToMsgId = other.replyToMsgId;
replyToMsgTop = other.replyToMsgTop;
replyToDocumentId = other.replyToDocumentId;
replyToWebPageId = other.replyToWebPageId;
std::swap(replyToMsg, other.replyToMsg);
replyToLnk = std::move(other.replyToLnk);
replyToName = std::move(other.replyToName);
@ -182,13 +183,14 @@ struct HistoryMessageReply : public RuntimeComponent<HistoryMessageReply, Histor
void setReplyToLinkFrom(
not_null<HistoryMessage*> 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;

View File

@ -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<HistoryMessageReply>()) {
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<HistoryMessageReply>()) {
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));
}
}
}

View File

@ -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,

View File

@ -1322,11 +1322,9 @@ void Gif::refreshParentId(not_null<HistoryItem*> 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(),

View File

@ -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<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,
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<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 text,
TimeId duration,
const QString &base) {
if (base.isEmpty()) {
return text;
}
static const auto expression = QRegularExpression(
"(?<![^\\s\\(\\)\"\\,\\.\\-])(?:(?:(\\d{1,2}):)?(\\d))?(\\d):(\\d\\d)(?![^\\s\\(\\)\",\\.\\-])");
const auto &string = text.text;
@ -104,10 +164,7 @@ TextWithEntities AddTimestampLinks(
EntityType::CustomUrl,
from,
till - from,
("internal:media_timestamp?base="
+ base
+ "&t="
+ QString::number(time))));
("internal:" + base + QString::number(time))));
}
return text;
}

View File

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

View File

@ -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(

View File

@ -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);
}