tdesktop/Telegram/SourceFiles/history/view/controls/history_view_webpage_proces...

400 lines
9.6 KiB
C++

/*
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 "history/view/controls/history_view_webpage_processor.h"
#include "base/unixtime.h"
#include "data/data_chat_participant_status.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
namespace HistoryView::Controls {
WebPageText TitleAndDescriptionFromWebPage(not_null<WebPageData*> d) {
QString resultTitle, resultDescription;
const auto document = d->document;
const auto author = d->author;
const auto siteName = d->siteName;
const auto title = d->title;
const auto description = d->description;
const auto filenameOrUrl = [&] {
return ((document && !document->filename().isEmpty())
? document->filename()
: d->url);
};
const auto authorOrFilename = [&] {
return (author.isEmpty()
? filenameOrUrl()
: author);
};
const auto descriptionOrAuthor = [&] {
return (description.text.isEmpty()
? authorOrFilename()
: description.text);
};
if (siteName.isEmpty()) {
if (title.isEmpty()) {
if (description.text.isEmpty()) {
resultTitle = author;
resultDescription = filenameOrUrl();
} else {
resultTitle = description.text;
resultDescription = authorOrFilename();
}
} else {
resultTitle = title;
resultDescription = descriptionOrAuthor();
}
} else {
resultTitle = siteName;
resultDescription = title.isEmpty()
? descriptionOrAuthor()
: title;
}
return { resultTitle, resultDescription };
}
bool DrawWebPageDataPreview(
QPainter &p,
not_null<WebPageData*> webpage,
not_null<PeerData*> context,
QRect to) {
const auto document = webpage->document;
const auto photo = webpage->photo;
if ((!photo || photo->isNull())
&& (!document
|| !document->hasThumbnail()
|| document->isPatternWallPaper())) {
return false;
}
const auto preview = photo
? photo->getReplyPreview(Data::FileOrigin(), context, false)
: document->getReplyPreview(Data::FileOrigin(), context, false);
if (preview) {
const auto w = preview->width();
const auto h = preview->height();
if (w == h) {
p.drawPixmap(to.x(), to.y(), preview->pix());
} else {
const auto from = (w > h)
? QRect((w - h) / 2, 0, h, h)
: QRect(0, (h - w) / 2, w, w);
p.drawPixmap(to, preview->pix(), from);
}
}
return true;
}
[[nodiscard]] bool ShowWebPagePreview(WebPageData *page) {
return page && !page->failed;
}
WebPageText ProcessWebPageData(WebPageData *page) {
auto previewText = TitleAndDescriptionFromWebPage(page);
if (previewText.title.isEmpty()) {
if (page->document) {
previewText.title = tr::lng_attach_file(tr::now);
} else if (page->photo) {
previewText.title = tr::lng_attach_photo(tr::now);
}
}
return previewText;
}
WebpageResolver::WebpageResolver(not_null<Main::Session*> session)
: _session(session)
, _api(&session->mtp()) {
}
std::optional<WebPageData*> WebpageResolver::lookup(
const QString &link) const {
const auto i = _cache.find(link);
return (i == end(_cache))
? std::optional<WebPageData*>()
: (i->second && !i->second->failed)
? i->second
: nullptr;
}
QString WebpageResolver::find(not_null<WebPageData*> page) const {
for (const auto &[link, cached] : _cache) {
if (cached == page) {
return link;
}
}
return QString();
}
void WebpageResolver::request(const QString &link, bool force) {
if (_requestLink == link && !force) {
return;
}
const auto done = [=](const MTPDmessageMediaWebPage &data) {
const auto page = _session->data().processWebpage(data.vwebpage());
if (page->pendingTill > 0
&& page->pendingTill < base::unixtime::now()) {
page->pendingTill = 0;
page->failed = true;
}
_cache.emplace(link, page->failed ? nullptr : page.get());
_resolved.fire_copy(link);
};
const auto fail = [=] {
_cache.emplace(link, nullptr);
_resolved.fire_copy(link);
};
_requestLink = link;
_requestId = _api.request(
MTPmessages_GetWebPagePreview(
MTP_flags(0),
MTP_string(link),
MTPVector<MTPMessageEntity>()
)).done([=](const MTPMessageMedia &result, mtpRequestId requestId) {
if (_requestId == requestId) {
_requestId = 0;
}
result.match([=](const MTPDmessageMediaWebPage &data) {
done(data);
}, [&](const auto &d) {
fail();
});
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
if (_requestId == requestId) {
_requestId = 0;
}
fail();
}).send();
}
void WebpageResolver::cancel(const QString &link) {
if (_requestLink == link) {
_api.request(base::take(_requestId)).cancel();
}
}
WebpageProcessor::WebpageProcessor(
not_null<History*> history,
not_null<Ui::InputField*> field)
: _history(history)
, _resolver(std::make_shared<WebpageResolver>(&history->session()))
, _parser(field)
, _timer([=] {
if (!ShowWebPagePreview(_data) || _link.isEmpty()) {
return;
}
_resolver->request(_link, true);
}) {
_history->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _data && (_data->document || _data->photo);
}) | rpl::start_with_next([=] {
_repaintRequests.fire({});
}, _lifetime);
_history->owner().webPageUpdates(
) | rpl::filter([=](not_null<WebPageData*> page) {
return (_data == page.get());
}) | rpl::start_with_next([=] {
updateFromData();
}, _lifetime);
_parser.list().changes(
) | rpl::start_with_next([=](QStringList &&parsed) {
_parsedLinks = std::move(parsed);
checkPreview();
}, _lifetime);
_resolver->resolved() | rpl::start_with_next([=](QString link) {
if (_link != link
|| _draft.removed
|| (_draft.manual && _draft.url != link)) {
return;
}
_data = _resolver->lookup(link).value_or(nullptr);
if (_data) {
_draft.id = _data->id;
_draft.url = _data->url;
updateFromData();
} else {
_links = QStringList();
checkPreview();
}
}, _lifetime);
}
rpl::producer<> WebpageProcessor::repaintRequests() const {
return _repaintRequests.events();
}
Data::WebPageDraft WebpageProcessor::draft() const {
return _draft;
}
std::shared_ptr<WebpageResolver> WebpageProcessor::resolver() const {
return _resolver;
}
const std::vector<MessageLinkRange> &WebpageProcessor::links() const {
return _parser.ranges();
}
QString WebpageProcessor::link() const {
return _link;
}
void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) {
const auto was = _link;
if (draft.removed) {
_draft = draft;
if (_parsedLinks.empty()) {
_draft.removed = false;
}
_data = nullptr;
_links = QStringList();
_link = QString();
_parsed = WebpageParsed();
updateFromData();
} else if (draft.manual && !draft.url.isEmpty()) {
_draft = draft;
_parsedLinks = QStringList();
_links = QStringList();
_link = _draft.url;
const auto page = draft.id
? _history->owner().webpage(draft.id).get()
: nullptr;
if (page && page->url == draft.url) {
_data = page;
if (const auto link = _resolver->find(page); !link.isEmpty()) {
_link = link;
}
updateFromData();
} else {
_resolver->request(_link);
return;
}
} else if (!draft.manual && !_draft.manual) {
_draft = draft;
checkNow(reparse);
}
if (_link != was) {
_resolver->cancel(was);
}
}
void WebpageProcessor::updateFromData() {
_timer.cancel();
auto parsed = WebpageParsed();
if (ShowWebPagePreview(_data)) {
if (const auto till = _data->pendingTill) {
parsed.drawPreview = [](QPainter &p, QRect to) {
return false;
};
parsed.title = tr::lng_preview_loading(tr::now);
parsed.description = _link;
const auto timeout = till - base::unixtime::now();
_timer.callOnce(
std::max(timeout, 0) * crl::time(1000));
} else {
const auto webpage = _data;
const auto context = _history->peer;
const auto preview = ProcessWebPageData(_data);
parsed.title = preview.title;
parsed.description = preview.description;
parsed.drawPreview = [=](QPainter &p, QRect to) {
return DrawWebPageDataPreview(p, webpage, context, to);
};
}
}
_parsed = std::move(parsed);
_repaintRequests.fire({});
}
void WebpageProcessor::setDisabled(bool disabled) {
_parser.setDisabled(disabled);
if (disabled) {
apply({ .removed = true });
} else {
checkNow(false);
}
}
void WebpageProcessor::checkNow(bool force) {
_parser.parseNow();
if (force) {
_link = QString();
_links = QStringList();
if (_parsedLinks.isEmpty()) {
_data = nullptr;
updateFromData();
return;
}
}
checkPreview();
}
void WebpageProcessor::checkPreview() {
const auto previewRestricted = _history->peer
&& _history->peer->amRestricted(ChatRestriction::EmbedLinks);
if (_parsedLinks.empty()) {
_draft.removed = false;
}
if (_draft.removed) {
return;
} else if (previewRestricted) {
apply({ .removed = true });
_draft.removed = false;
return;
} else if (_draft.manual) {
return;
} else if (_links == _parsedLinks) {
return;
}
_links = _parsedLinks;
auto page = (WebPageData*)nullptr;
auto chosen = QString();
for (const auto &link : _links) {
const auto value = _resolver->lookup(link);
if (!value) {
chosen = link;
break;
} else if (*value) {
chosen = link;
page = *value;
break;
}
}
if (_link != chosen) {
_resolver->cancel(_link);
_link = chosen;
if (!page && !_link.isEmpty()) {
_resolver->request(_link);
}
}
if (page) {
_data = page;
_draft.id = _data->id;
_draft.url = _data->url;
} else {
_data = nullptr;
_draft = {};
}
updateFromData();
}
rpl::producer<WebpageParsed> WebpageProcessor::parsedValue() const {
return _parsed.value();
}
} // namespace HistoryView::Controls