Files
AyuGramDesktop/Telegram/SourceFiles/iv/iv_instance.cpp
T
2026-06-23 11:28:52 +04:00

1675 lines
45 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 "iv/iv_instance.h"
#include "apiwrap.h"
#include "base/platform/base_platform_info.h"
#include "base/unixtime.h"
#include "boxes/share_box.h"
#include "core/application.h"
#include "core/file_utilities.h"
#include "core/shortcuts.h"
#include "core/click_handler_types.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "history/history.h"
#include "history/history_item.h"
#include "iv/markdown/iv_markdown_article.h"
#include "iv/markdown/iv_markdown_controller.h"
#include "iv/iv_cached_media.h"
#include "iv/iv_controller.h"
#include "iv/iv_data.h"
#include "iv/iv_rich_page.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "main/session/session_show.h"
#include "media/view/media_view_open_common.h"
#include "storage/storage_account.h"
#include "ui/toast/toast.h"
#include "ui/basic_click_handlers.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "window/window_session_controller_link_info.h"
#include "styles/style_iv.h"
#include <QtCore/QByteArray>
#include <QtCore/QFileInfo>
#include <optional>
namespace Iv {
namespace {
constexpr auto kAllowPageReloadAfter = 3 * crl::time(1000);
struct NativeIvChannelContext {
uint64 channelId = 0;
QString username;
};
[[nodiscard]] NativeIvChannelContext ParseNativeIvChannelContext(
const QString &context) {
const auto separator = context.indexOf(u'\n');
return {
.channelId = (separator >= 0)
? context.mid(0, separator).toULongLong()
: context.toULongLong(),
.username = (separator >= 0) ? context.mid(separator + 1) : QString(),
};
}
[[nodiscard]] QString ResolveNativeIvChannelUsername(
const QString &channelUsername,
const QString &contextUsername) {
return !channelUsername.isEmpty() ? channelUsername : contextUsername;
}
struct MarkdownMessageContext {
ClickHandlerContext clickHandlerContext;
base::weak_ptr<Window::SessionController> sessionWindow;
};
struct LocalMarkdownTarget {
QString key;
QString path;
QString sourceName;
QString fragment;
};
[[nodiscard]] QString NormalizeLocalMarkdownFragment(QString fragment) {
fragment = QString::fromUtf8(
QByteArray::fromPercentEncoding(fragment.toUtf8()));
fragment = fragment.trimmed().toLower();
while (fragment.startsWith(QChar('#'))) {
fragment.remove(0, 1);
}
return fragment;
}
[[nodiscard]] LocalMarkdownTarget ParseLocalMarkdownTarget(QString path) {
auto sourcePath = path;
auto fragment = QString();
if (!QFileInfo(sourcePath).exists()) {
const auto hash = sourcePath.lastIndexOf(QChar('#'));
const auto candidate = (hash > 0) ? sourcePath.mid(0, hash) : QString();
if (!candidate.isEmpty() && QFileInfo(candidate).exists()) {
fragment = NormalizeLocalMarkdownFragment(sourcePath.mid(hash + 1));
sourcePath = candidate;
}
}
const auto info = QFileInfo(sourcePath);
if (!info.exists()) {
return {
.key = path,
.path = std::move(path),
};
}
auto result = LocalMarkdownTarget{
.key = info.absoluteFilePath(),
.path = info.absoluteFilePath(),
.sourceName = info.fileName(),
.fragment = std::move(fragment),
};
if (!result.fragment.isEmpty()) {
result.path += u"#"_q + result.fragment;
}
return result;
}
[[nodiscard]] auto ExtractMarkdownMessageContext(const QVariant &context) {
if (!context.isValid() || !context.canConvert<ClickHandlerContext>()) {
return std::optional<MarkdownMessageContext>();
}
const auto clickHandlerContext = context.value<ClickHandlerContext>();
return std::make_optional(MarkdownMessageContext{
.clickHandlerContext = clickHandlerContext,
.sessionWindow = clickHandlerContext.sessionWindow,
});
}
[[nodiscard]] Main::Session *ResolveMarkdownSession(
const MarkdownMessageContext &context) {
if (const auto controller = context.sessionWindow.get()) {
return &controller->session();
}
return nullptr;
}
[[nodiscard]] HistoryItem *ResolveMarkdownItem(
const MarkdownMessageContext &context) {
const auto session = ResolveMarkdownSession(context);
const auto itemId = context.clickHandlerContext.itemId;
return (session && itemId) ? session->data().message(itemId) : nullptr;
}
[[nodiscard]] bool CanShareMarkdownItem(not_null<HistoryItem*> item) {
const auto peer = item->history()->peer;
return peer->allowsForwarding() && !item->forbidsForward();
}
[[nodiscard]] QString RichMessageKey(FullMsgId itemId) {
return u"rich-message:%1:%2"_q
.arg(itemId.peer.value)
.arg(itemId.msg.bare);
}
[[nodiscard]] uint64 RichMessagePageId(FullMsgId itemId) {
return uint64(itemId.peer.value) ^ (uint64(itemId.msg.bare) << 1);
}
[[nodiscard]] bool PreparedContentHasAnchor(
const Markdown::MarkdownArticleContent &content,
const QString &anchorId) {
if (anchorId.isEmpty()) {
return false;
}
auto article = Markdown::MarkdownArticle(st::defaultMarkdown);
article.setContent(content);
const auto width = article.maxWidth();
static_cast<void>(article.resizeGetHeight(width));
auto top = article.anchorTop(anchorId);
if (top < 0) {
const auto expansion = article.expandDetailsToAnchor(anchorId);
if (expansion.changed) {
static_cast<void>(article.resizeGetHeight(width));
}
top = article.anchorTop(anchorId);
}
return (top >= 0);
}
void OpenRichMessageChannel(
not_null<Main::Session*> session,
const QString &context) {
const auto parsed = ParseNativeIvChannelContext(context);
if (const auto channelId = ChannelId(parsed.channelId)) {
const auto channel = session->data().channel(channelId);
if (channel->isLoaded()) {
if (const auto controller = session->tryResolveWindow(channel)) {
controller->showPeerHistory(channel);
}
} else if (const auto username = ResolveNativeIvChannelUsername(
channel->username(),
parsed.username); !username.isEmpty()) {
if (const auto controller = session->tryResolveWindow(channel)) {
controller->showPeerByLink({
.usernameOrId = username,
});
}
}
}
}
void JoinRichMessageChannel(
not_null<Main::Session*> session,
const QString &context) {
const auto parsed = ParseNativeIvChannelContext(context);
if (const auto channelId = ChannelId(parsed.channelId)) {
const auto channel = session->data().channel(channelId);
if (channel->isLoaded()) {
session->api().joinChannel(channel);
} else if (const auto username = ResolveNativeIvChannelUsername(
channel->username(),
parsed.username); !username.isEmpty()) {
if (const auto controller = session->tryResolveWindow(channel)) {
controller->showPeerByLink({
.usernameOrId = username,
.joinChannel = true,
});
}
}
}
}
[[nodiscard]] bool ActivateRichMessageMedia(
const Markdown::MediaActivation &activation,
Qt::MouseButton button,
const QVariant &clickHandlerContext) {
if (button != Qt::LeftButton && button != Qt::MiddleButton) {
return false;
}
switch (activation.kind) {
case Markdown::MediaActivationKind::None:
return false;
case Markdown::MediaActivationKind::ExternalUrl:
if (activation.url.isEmpty()) {
return false;
}
HiddenUrlClickHandler::Open(activation.url, clickHandlerContext);
return true;
case Markdown::MediaActivationKind::Embed:
return false;
case Markdown::MediaActivationKind::Photo:
if (!activation.photo) {
return false;
}
activation.photo->open(button);
return true;
case Markdown::MediaActivationKind::Document:
if (!activation.document) {
return false;
}
activation.document->open(button);
return true;
case Markdown::MediaActivationKind::OpenChannel:
if (!activation.channel) {
return false;
}
activation.channel->open(button);
return true;
case Markdown::MediaActivationKind::JoinChannel:
if (!activation.channel) {
return false;
}
activation.channel->join(button);
return true;
}
return false;
}
[[nodiscard]] Markdown::OpenOptions PrepareLocalMarkdownOptions(
QVariant context) {
auto options = Markdown::OpenOptions{
.viewerKind = Markdown::ViewerKind::LocalFile,
.clickHandlerContext = std::move(context),
};
const auto messageContext = ExtractMarkdownMessageContext(
options.clickHandlerContext);
const auto item = messageContext
? ResolveMarkdownItem(*messageContext)
: nullptr;
if (item && CanShareMarkdownItem(not_null{ item })) {
options.share = [context = *messageContext](
std::shared_ptr<Ui::Show> show) {
const auto session = ResolveMarkdownSession(context);
const auto itemId = context.clickHandlerContext.itemId;
const auto current = (session && itemId)
? session->data().message(itemId)
: nullptr;
if (!show || !current || !CanShareMarkdownItem(not_null{ current })) {
return;
}
FastShareMessage(
Main::MakeSessionShow(show, not_null{ session }),
not_null{ current });
};
}
return options;
}
} // namespace
class Shown final : public base::has_weak_ptr {
public:
Shown(
not_null<Delegate*> delegate,
not_null<Main::Session*> session,
not_null<Data*> data,
QString hash,
Fn<void(QString)> openChannel,
Fn<void(QString)> joinChannel);
[[nodiscard]] bool showing(
not_null<Main::Session*> session,
not_null<Data*> data) const;
[[nodiscard]] bool showingFrom(not_null<Main::Session*> session) const;
[[nodiscard]] bool activeFor(not_null<Main::Session*> session) const;
[[nodiscard]] bool active() const;
void moveTo(not_null<Data*> data, QString hash);
void update(not_null<Data*> data);
void showJoinedTooltip();
void minimize();
[[nodiscard]] rpl::producer<Controller::Event> events() const {
return _events.events();
}
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private:
void prepare(not_null<Data*> data, const QString &hash);
void createMarkdownController(
Markdown::MarkdownArticleContent content,
QString title,
QString initialFragment,
not_null<WebPageData*> page);
[[nodiscard]] Markdown::OpenOptions markdownOpenOptions(
QString initialFragment,
not_null<WebPageData*> page);
void showMarkdownWindowed(
Markdown::MarkdownArticleContent content,
QString title,
QString initialFragment,
not_null<WebPageData*> page,
bool refresh);
[[nodiscard]] std::shared_ptr<Markdown::MediaRuntime> createMediaRuntime(
not_null<WebPageData*> page);
[[nodiscard]] bool activateMarkdownMedia(
const Markdown::MediaActivation &activation,
Qt::MouseButton button,
const QVariant &clickHandlerContext) const;
const not_null<Delegate*> _delegate;
const not_null<Main::Session*> _session;
const Fn<void(QString)> _openChannel;
const Fn<void(QString)> _joinChannel;
QString _id;
std::unique_ptr<Markdown::Controller> _markdownController;
std::shared_ptr<Markdown::MediaRuntime> _markdownMediaRuntime;
QString _markdownRuntimeUrl;
rpl::event_stream<Controller::Event> _events;
rpl::lifetime _lifetime;
};
class TonSite final : public base::has_weak_ptr {
public:
TonSite(not_null<Delegate*> delegate, QString uri);
[[nodiscard]] bool active() const;
void moveTo(QString uri);
void minimize();
[[nodiscard]] rpl::producer<Controller::Event> events() const {
return _events.events();
}
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private:
void createController();
void showWindowed();
const not_null<Delegate*> _delegate;
QString _uri;
std::unique_ptr<Controller> _controller;
rpl::event_stream<Controller::Event> _events;
rpl::lifetime _lifetime;
};
Shown::Shown(
not_null<Delegate*> delegate,
not_null<Main::Session*> session,
not_null<Data*> data,
QString hash,
Fn<void(QString)> openChannel,
Fn<void(QString)> joinChannel)
: _delegate(delegate)
, _session(session)
, _openChannel(std::move(openChannel))
, _joinChannel(std::move(joinChannel)) {
prepare(data, hash);
}
void Shown::prepare(not_null<Data*> data, const QString &hash) {
const auto richPage = data->richPage();
const auto id = data->id();
const auto page = _session->data().webpage(data->pageId());
if (_markdownRuntimeUrl != id) {
_markdownMediaRuntime = nullptr;
_markdownRuntimeUrl = QString();
}
_id = id;
auto prepared = Markdown::TryPrepareNativeInstantView({
.richPage = richPage,
.mediaRuntime = createMediaRuntime(page),
});
showMarkdownWindowed(
std::move(prepared.content),
data->name(),
hash,
page,
false);
}
Markdown::OpenOptions Shown::markdownOpenOptions(
QString initialFragment,
not_null<WebPageData*> page) {
const auto clickHandlerContext = std::make_shared<QVariant>();
auto options = Markdown::OpenOptions{
.sourceName = page->displayedSiteName(),
.sourceUrl = page->url,
.initialFragment = std::move(initialFragment),
.currentPageId = page->id,
.viewerKind = Markdown::ViewerKind::InstantView,
.clickHandlerContextRef = clickHandlerContext,
.ivWebviewStorageId = _session->local().resolveStorageIdOther(),
.activateMedia = [=](
const Markdown::MediaActivation &activation,
Qt::MouseButton button) {
return activateMarkdownMedia(activation, button, *clickHandlerContext);
},
.downloadTaskFinished = page->session().downloaderTaskFinished(),
};
if (!page->url.isEmpty()) {
options.share = [=, url = page->url](std::shared_ptr<Ui::Show> show) {
if (!show) {
return;
}
FastShareLink(Main::MakeSessionShow(show, _session), url);
};
}
return options;
}
void Shown::createMarkdownController(
Markdown::MarkdownArticleContent content,
QString title,
QString initialFragment,
not_null<WebPageData*> page) {
Expects(!_markdownController);
auto options = markdownOpenOptions(std::move(initialFragment), page);
_markdownController = std::make_unique<Markdown::Controller>(
_delegate,
std::move(content),
std::move(title),
nullptr,
std::move(options));
_markdownController->events() | rpl::on_next([=](Markdown::Event event) {
using FromType = Markdown::Event::Type;
using ToType = Controller::Event::Type;
switch (event.type) {
case FromType::Close:
_events.fire({ .type = ToType::Close });
break;
case FromType::Quit:
_events.fire({ .type = ToType::Quit });
break;
case FromType::OpenPage:
_events.fire({
.type = ToType::OpenPage,
.url = event.url,
.context = QString::number(event.webpageId),
.webpageId = event.webpageId,
});
break;
case FromType::OpenFile:
break;
case FromType::Report:
_events.fire({
.type = ToType::Report,
.context = QString::number(event.webpageId),
});
break;
}
}, _markdownController->lifetime());
}
void Shown::showMarkdownWindowed(
Markdown::MarkdownArticleContent content,
QString title,
QString initialFragment,
not_null<WebPageData*> page,
bool refresh) {
if (!_markdownController) {
createMarkdownController(
std::move(content),
std::move(title),
std::move(initialFragment),
page);
_markdownController->activate();
return;
}
auto options = markdownOpenOptions(std::move(initialFragment), page);
if (refresh) {
_markdownController->update(
std::move(content),
std::move(title),
std::move(options));
} else {
_markdownController->show(
std::move(content),
std::move(title),
std::move(options));
}
}
std::shared_ptr<Markdown::MediaRuntime> Shown::createMediaRuntime(
not_null<WebPageData*> page) {
if (_markdownMediaRuntime && (page->url == _markdownRuntimeUrl)) {
return _markdownMediaRuntime;
}
_markdownRuntimeUrl = page->url;
_markdownMediaRuntime = CreateCachedPageMediaRuntime(
_session,
page,
_openChannel,
_joinChannel);
return _markdownMediaRuntime;
}
bool Shown::activateMarkdownMedia(
const Markdown::MediaActivation &activation,
Qt::MouseButton button,
const QVariant &clickHandlerContext) const {
if (button != Qt::LeftButton && button != Qt::MiddleButton) {
return false;
}
switch (activation.kind) {
case Markdown::MediaActivationKind::None:
return false;
case Markdown::MediaActivationKind::ExternalUrl:
if (activation.url.isEmpty()) {
return false;
}
HiddenUrlClickHandler::Open(activation.url, clickHandlerContext);
return true;
case Markdown::MediaActivationKind::Embed:
return false;
case Markdown::MediaActivationKind::Photo:
if (!activation.photo) {
return false;
}
activation.photo->open(button);
return true;
case Markdown::MediaActivationKind::Document:
if (!activation.document) {
return false;
}
activation.document->open(button);
return true;
case Markdown::MediaActivationKind::OpenChannel:
if (!activation.channel) {
return false;
}
activation.channel->open(button);
return true;
case Markdown::MediaActivationKind::JoinChannel:
if (!activation.channel) {
return false;
}
activation.channel->join(button);
return true;
}
return false;
}
bool Shown::showing(
not_null<Main::Session*> session,
not_null<Data*> data) const {
return showingFrom(session) && (_id == data->id());
}
bool Shown::showingFrom(not_null<Main::Session*> session) const {
return (_session == session);
}
bool Shown::activeFor(not_null<Main::Session*> session) const {
return showingFrom(session) && _markdownController;
}
bool Shown::active() const {
return _markdownController && _markdownController->active();
}
void Shown::moveTo(not_null<Data*> data, QString hash) {
prepare(data, hash);
}
void Shown::update(not_null<Data*> data) {
const auto richPage = data->richPage();
const auto id = data->id();
if (_markdownRuntimeUrl != id) {
_markdownMediaRuntime = nullptr;
_markdownRuntimeUrl = QString();
}
_id = id;
const auto page = _session->data().webpage(data->pageId());
auto prepared = Markdown::TryPrepareNativeInstantView({
.richPage = richPage,
.mediaRuntime = createMediaRuntime(page),
});
showMarkdownWindowed(
std::move(prepared.content),
data->name(),
QString(),
page,
true);
}
void Shown::showJoinedTooltip() {
if (_markdownController) {
_markdownController->showJoinedTooltip();
}
}
void Shown::minimize() {
if (_markdownController) {
_markdownController->minimize();
}
}
TonSite::TonSite(not_null<Delegate*> delegate, QString uri)
: _delegate(delegate)
, _uri(uri) {
showWindowed();
}
void TonSite::createController() {
Expects(!_controller);
_controller = std::make_unique<Controller>(_delegate);
_controller->events(
) | rpl::start_to_stream(_events, _controller->lifetime());
}
void TonSite::showWindowed() {
if (!_controller) {
createController();
}
_controller->showTonSite(Storage::TonSiteStorageId(), _uri);
}
bool TonSite::active() const {
return _controller && _controller->active();
}
void TonSite::moveTo(QString uri) {
_controller->showTonSite({}, uri);
}
void TonSite::minimize() {
if (_controller) {
_controller->minimize();
}
}
Instance::Instance(not_null<Delegate*> delegate) : _delegate(delegate) {
}
Instance::~Instance() = default;
void Instance::show(
not_null<Window::SessionController*> controller,
not_null<Data*> data,
QString hash) {
showOpenedPage(&controller->session(), data, std::move(hash), true);
}
void Instance::show(
std::shared_ptr<Main::SessionShow> show,
not_null<Data*> data,
QString hash) {
showOpenedPage(&show->session(), data, std::move(hash), true);
}
void Instance::show(
not_null<Main::Session*> session,
not_null<Data*> data,
QString hash) {
showOpenedPage(session, data, std::move(hash), true);
}
void Instance::showOpenedPage(
not_null<Main::Session*> session,
not_null<Data*> data,
QString hash,
bool requestFullOnOpen) {
if (Platform::IsMac()) {
// Otherwise IV is not visible under the media viewer.
Core::App().hideMediaView();
}
if (Core::App().settings().normalizeIvZoom()) {
Core::App().saveSettingsDelayed();
}
primeFullRequest(session, data);
const auto guard = gsl::finally([&] {
if (requestFullOnOpen) {
requestFull(session, data->id());
}
});
if (_shown && _shownSession == session) {
_shown->moveTo(data, hash);
return;
}
_shown = std::make_unique<Shown>(
_delegate,
session,
data,
hash,
[=](QString context) {
processOpenChannel(context);
},
[=](QString context) {
processJoinChannel(context);
});
_shownSession = session;
_shown->events() | rpl::on_next([=](Controller::Event event) {
using Type = Controller::Event::Type;
const auto lower = event.url.toLower();
const auto urlChecked = lower.startsWith("http://")
|| lower.startsWith("https://");
const auto tonsite = lower.startsWith("tonsite://");
switch (event.type) {
case Type::Close:
_shown = nullptr;
break;
case Type::Quit:
Shortcuts::Launch(Shortcuts::Command::Quit);
break;
case Type::OpenChannel:
processOpenChannel(event.context);
break;
case Type::JoinChannel:
processJoinChannel(event.context);
break;
case Type::OpenLinkExternal:
if (urlChecked) {
File::OpenUrl(event.url);
closeAll();
} else if (tonsite) {
showTonSite(event.url);
}
break;
case Type::OpenMedia:
if (const auto window = Core::App().activeWindow()) {
const auto current = window->sessionController();
const auto controller = (current
&& &current->session() == _shownSession)
? current
: nullptr;
const auto item = (HistoryItem*)nullptr;
const auto topicRootId = MsgId(0);
const auto monoforumPeerId = PeerId(0);
if (event.context.startsWith("-photo")) {
const auto id = event.context.mid(6).toULongLong();
const auto photo = _shownSession->data().photo(id);
if (!photo->isNull()) {
window->openInMediaView({
controller,
photo,
item,
topicRootId,
monoforumPeerId
});
}
} else if (event.context.startsWith("-video")) {
const auto id = event.context.mid(6).toULongLong();
const auto video = _shownSession->data().document(id);
if (!video->isNull()) {
window->openInMediaView({
controller,
video,
item,
topicRootId,
monoforumPeerId
});
}
}
}
break;
case Type::OpenPage:
case Type::OpenLink: {
if (tonsite) {
showTonSite(event.url);
break;
} else if (!urlChecked) {
break;
}
const auto session = _shownSession;
const auto url = event.url;
const auto parts = event.url.split('#');
const auto hash = (parts.size() > 1) ? parts[1] : u""_q;
if (event.webpageId) {
const auto page = session->data().webpage(
WebPageId(event.webpageId)).get();
if (page->iv) {
this->showOpenedPage(session, page->iv.get(), hash, false);
break;
}
}
const auto requestKey = event.webpageId
? QString::number(event.webpageId)
: url;
auto &requested = _fullRequested[session][requestKey];
if (event.webpageId) {
const auto page = session->data().webpage(
WebPageId(event.webpageId)).get();
if (page->iv) {
requested.page = page;
requested.hash = page->iv->hash();
} else {
requested.hash = 0;
}
}
requested.lastRequestedAt = crl::now();
session->api().request(MTPmessages_GetWebPage(
MTP_string(url),
MTP_int(requested.hash)
)).done([=](const MTPmessages_WebPage &result) {
const auto processed = processReceivedPage(
session,
requestKey,
result);
if (const auto page = processed.page; page && page->iv) {
if (event.webpageId && page->id != event.webpageId) {
const auto expected = session->data().webpage(
WebPageId(event.webpageId)).get();
if (expected->iv) {
this->showOpenedPage(
session,
expected->iv.get(),
hash,
false);
} else {
UrlClickHandler::Open(event.url);
}
return;
}
this->showOpenedPage(session, page->iv.get(), hash, false);
} else {
UrlClickHandler::Open(event.url);
}
}).fail([=] {
UrlClickHandler::Open(event.url);
}).send();
} break;
case Type::Report:
if (const auto controller = _shownSession->tryResolveWindow()) {
controller->window().activate();
controller->showPeerByLink(Window::PeerByLinkInfo{
.usernameOrId = "previews",
.resolveType = Window::ResolveType::BotStart,
.startToken = ("webpage"
+ QString::number(event.context.toULongLong())),
});
}
break;
}
}, _shown->lifetime());
session->changes().peerUpdates(
::Data::PeerUpdate::Flag::ChannelAmIn
) | rpl::on_next([=](const ::Data::PeerUpdate &update) {
if (const auto channel = update.peer->asChannel()) {
if (channel->amIn()) {
const auto i = _joining.find(session);
const auto value = not_null{ channel };
if (i != end(_joining) && i->second.remove(value)) {
_shown->showJoinedTooltip();
}
}
}
}, _shown->lifetime());
trackSession(session);
}
void Instance::primeFullRequest(
not_null<Main::Session*> session,
not_null<Data*> data) {
auto &requested = _fullRequested[session][data->id()];
requested.page = session->data().webpage(data->pageId()).get();
requested.hash = data->hash();
}
void Instance::trackSession(not_null<Main::Session*> session) {
if (!_tracking.emplace(session).second) {
return;
}
session->data().sessionDataAboutToBeCleared(
) | rpl::on_next([=] {
closeSessionDataViews(session);
}, session->lifetime());
session->data().itemRemoved(
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
closeMarkdownsForItem(session, item->fullId());
}, session->lifetime());
session->data().itemsAboutToBeDestroyed(
) | rpl::on_next([=](std::vector<not_null<HistoryItem*>> items) {
for (const auto &item : items) {
closeMarkdownsForItem(session, item->fullId());
}
}, session->lifetime());
session->lifetime().add([=] {
closeSessionDataViews(session);
_tracking.remove(session);
_joining.remove(session);
_fullRequested.remove(session);
if (const auto i = _richMessageRequested.find(session)
; i != end(_richMessageRequested)) {
for (const auto &[itemId, requested] : i->second) {
if (requested.requestId) {
session->api().request(requested.requestId).cancel();
}
}
_richMessageRequested.erase(i);
}
_ivCache.remove(session);
if (_ivRequestSession == session) {
session->api().request(_ivRequestId).cancel();
_ivRequestSession = nullptr;
_ivRequestUri = QString();
_ivRequestId = 0;
}
});
}
void Instance::bindMarkdown(
const QString &key,
not_null<Main::Session*> session,
FullMsgId itemId) {
_markdownBindings[key] = {
.session = session.get(),
.itemId = itemId,
};
trackSession(session);
}
void Instance::closeMarkdownsForItem(
not_null<Main::Session*> session,
FullMsgId itemId) {
if (!itemId) {
return;
}
auto keys = std::vector<QString>();
for (const auto &[key, binding] : _markdownBindings) {
if (binding.session == session.get() && binding.itemId == itemId) {
keys.push_back(key);
}
}
for (const auto &key : keys) {
_markdownBindings.remove(key);
_markdowns.take(key);
}
}
void Instance::closeMarkdownsForSession(not_null<Main::Session*> session) {
auto keys = std::vector<QString>();
for (const auto &[key, binding] : _markdownBindings) {
if (binding.session == session.get()) {
keys.push_back(key);
}
}
for (const auto &key : keys) {
_markdownBindings.remove(key);
_markdowns.take(key);
}
}
void Instance::closeSessionDataViews(not_null<Main::Session*> session) {
closeMarkdownsForSession(session);
if (_shownSession == session) {
_shownSession = nullptr;
}
if (_shown && _shown->showingFrom(session)) {
_shown = nullptr;
}
}
void Instance::openWithIvPreferred(
not_null<Window::SessionController*> controller,
QString uri,
QVariant context) {
auto my = context.value<ClickHandlerContext>();
my.sessionWindow = controller;
openWithIvPreferred(
&controller->session(),
uri,
QVariant::fromValue(my));
}
void Instance::openWithIvPreferred(
not_null<Main::Session*> session,
QString uri,
QVariant context) {
const auto openExternal = [=] {
auto my = context.value<ClickHandlerContext>();
my.ignoreIv = true;
const auto updated = QVariant::fromValue(my);
if (my.forceExternalUrlConfirmation) {
HiddenUrlClickHandler::Open(uri, updated);
} else {
UrlClickHandler::Open(uri, updated);
}
};
const auto parts = uri.split('#');
if (parts.isEmpty() || parts[0].isEmpty()) {
return;
}
trackSession(session);
const auto hash = (parts.size() > 1) ? parts[1] : u""_q;
const auto url = parts[0];
auto &cache = _ivCache[session];
if (const auto i = cache.find(url); i != end(cache)) {
const auto page = i->second;
if (page && page->iv) {
auto my = context.value<ClickHandlerContext>();
if (const auto window = my.sessionWindow.get()) {
show(window, page->iv.get(), hash);
} else {
show(session, page->iv.get(), hash);
}
} else {
openExternal();
}
return;
} else if (_ivRequestSession == session.get() && _ivRequestUri == uri) {
return;
} else if (_ivRequestId) {
_ivRequestSession->api().request(_ivRequestId).cancel();
}
const auto finish = [=](WebPageData *page) {
Expects(_ivRequestSession == session);
_ivRequestId = 0;
_ivRequestUri = QString();
_ivRequestSession = nullptr;
_ivCache[session][url] = page;
if (page && page->iv) {
this->showOpenedPage(session, page->iv.get(), hash, false);
} else {
openExternal();
}
};
_ivRequestSession = session;
_ivRequestUri = uri;
auto &requested = _fullRequested[session][url];
requested.lastRequestedAt = crl::now();
_ivRequestId = session->api().request(MTPmessages_GetWebPage(
MTP_string(url),
MTP_int(requested.hash)
)).done([=](const MTPmessages_WebPage &result) {
finish(processReceivedPage(session, url, result).page);
}).fail([=] {
finish(nullptr);
}).send();
}
void Instance::showTonSite(
const QString &uri,
QVariant context) {
if (!Controller::IsGoodTonSiteUrl(uri)) {
Ui::Toast::Show(tr::lng_iv_not_supported(tr::now));
return;
} else if (Platform::IsMac()) {
// Otherwise IV is not visible under the media viewer.
Core::App().hideMediaView();
}
if (_tonSite) {
_tonSite->moveTo(uri);
return;
}
_tonSite = std::make_unique<TonSite>(_delegate, uri);
_tonSite->events() | rpl::on_next([=](Controller::Event event) {
using Type = Controller::Event::Type;
const auto lower = event.url.toLower();
const auto urlChecked = lower.startsWith("http://")
|| lower.startsWith("https://");
const auto tonsite = lower.startsWith("tonsite://");
switch (event.type) {
case Type::Close:
_tonSite = nullptr;
break;
case Type::Quit:
Shortcuts::Launch(Shortcuts::Command::Quit);
break;
case Type::OpenLinkExternal:
if (urlChecked) {
File::OpenUrl(event.url);
closeAll();
} else if (tonsite) {
showTonSite(event.url);
}
break;
case Type::OpenPage:
case Type::OpenLink:
if (urlChecked) {
UrlClickHandler::Open(event.url);
} else if (tonsite) {
showTonSite(event.url);
}
break;
}
}, _tonSite->lifetime());
}
Instance::RichMessageGeneration Instance::CaptureRichMessageGeneration(
not_null<HistoryItem*> item) {
auto generation = RichMessageGeneration();
generation.inlinePage = item->richPage();
generation.fullPage = item->fullRichPage();
generation.fullPageVersion = item->fullRichPageVersion();
return generation;
}
bool Instance::MatchesRichMessageGeneration(
not_null<HistoryItem*> item,
const RichMessageGeneration &generation) {
return (item->richPage() == generation.inlinePage)
&& (item->fullRichPage() == generation.fullPage)
&& (item->fullRichPageVersion() == generation.fullPageVersion);
}
void Instance::resolveRichMessage(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
RichMessageResolved done) {
if (const auto page = item->fullRichPage()) {
done(page);
return;
}
const auto richPage = item->richPage();
if (richPage && !richPage->part) {
done(richPage);
return;
}
const auto session = &controller->session();
const auto itemId = item->fullId();
const auto generation = CaptureRichMessageGeneration(item);
trackSession(session);
auto &requested = _richMessageRequested[session][itemId];
if (requested.requestId) {
if (MatchesRichMessageGeneration(item, requested.generation)) {
requested.callbacks.push_back(std::move(done));
return;
}
session->api().request(requested.requestId).cancel();
requested = RichMessageRequest();
}
requested.callbacks.push_back(std::move(done));
const auto now = crl::now();
if (requested.lastRequestedAt
&& MatchesRichMessageGeneration(item, requested.generation)
&& (now - requested.lastRequestedAt) < kAllowPageReloadAfter) {
finishRichMessageRequest(session, itemId, requested.token, nullptr);
return;
}
requested.lastRequestedAt = now;
requested.generation = generation;
requested.token = ++_nextRichMessageRequestToken;
const auto token = requested.token;
requested.requestId = session->api().request(MTPmessages_GetRichMessage(
item->history()->peer->input(),
MTP_int(item->id)
)).done([=](const MTPmessages_Messages &result) {
const auto processed = processReceivedRichMessage(
session,
itemId,
generation,
result);
finishRichMessageRequest(
session,
itemId,
token,
processed.page,
processed.notifyCallbacks);
}).fail([=] {
finishRichMessageRequest(
session,
itemId,
token,
nullptr);
}).send();
}
void Instance::showRichMessage(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
QString initialFragment) {
const auto weak = base::make_weak(controller);
const auto itemId = item->fullId();
resolveRichMessage(controller, item, [=](std::shared_ptr<const RichPage> page) {
const auto strong = weak.get();
const auto current = strong
? strong->session().data().message(itemId)
: nullptr;
if (!page || !current) {
if (strong && !page) {
Ui::Toast::Show(tr::lng_iv_not_supported(tr::now));
}
return;
}
showRichMessage(
not_null{ strong },
not_null{ current },
std::move(page),
initialFragment);
});
}
void Instance::showRichMessage(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
std::shared_ptr<const RichPage> richPage,
QString initialFragment) {
if (Platform::IsMac()) {
Core::App().hideMediaView();
}
const auto session = &controller->session();
const auto itemId = item->fullId();
const auto key = RichMessageKey(itemId);
const auto title = item->history()->peer->name();
auto clickHandlerContext = ClickHandlerContext();
clickHandlerContext.sessionWindow = controller;
clickHandlerContext.itemId = itemId;
auto context = QVariant::fromValue(clickHandlerContext);
auto mediaRuntime = CreateMessageMediaRuntime(
session,
itemId,
[=](QString context) {
OpenRichMessageChannel(session, context);
},
[=](QString context) {
JoinRichMessageChannel(session, context);
});
const auto richLimits = ResolveRichMessageLimits(session);
auto prepared = Markdown::TryPrepareNativeInstantView({
.richPage = richPage,
.mediaRuntime = std::move(mediaRuntime),
.tableRenderLimits = Markdown::PrepareTableRenderLimitsForRichMessage(
richLimits),
});
if (!prepared.supported()) {
Ui::Toast::Show(tr::lng_iv_not_supported(tr::now));
return;
}
if (!initialFragment.isEmpty()
&& !PreparedContentHasAnchor(prepared.content, initialFragment)) {
controller->showToast(tr::lng_iv_not_found_in_message(tr::now));
return;
}
auto options = Markdown::OpenOptions{
.sourceName = title,
.currentPageId = RichMessagePageId(itemId),
.viewerKind = Markdown::ViewerKind::InstantView,
.clickHandlerContext = context,
.activateMedia = [=](
const Markdown::MediaActivation &activation,
Qt::MouseButton button) {
return ActivateRichMessageMedia(activation, button, context);
},
.downloadTaskFinished = session->downloaderTaskFinished(),
};
options.initialFragment = std::move(initialFragment);
if (CanShareMarkdownItem(item)) {
options.share = [=](std::shared_ptr<Ui::Show> show) {
const auto current = session->data().message(itemId);
if (!show || !current || !CanShareMarkdownItem(not_null{ current })) {
return;
}
FastShareMessage(
Main::MakeSessionShow(show, not_null{ session }),
not_null{ current });
};
}
auto i = _markdowns.find(key);
if (i == end(_markdowns)) {
auto controller = std::make_unique<Markdown::Controller>(
_delegate,
std::move(prepared.content),
title,
nullptr,
std::move(options));
controller->events() | rpl::on_next([=](Markdown::Event event) {
using Type = Markdown::Event::Type;
switch (event.type) {
case Type::Close:
_markdownBindings.remove(key);
_markdowns.take(key);
break;
case Type::Quit:
Shortcuts::Launch(Shortcuts::Command::Quit);
break;
case Type::OpenPage:
case Type::OpenFile:
if (!event.url.isEmpty()) {
UrlClickHandler::Open(event.url, event.context);
}
break;
case Type::Report:
break;
}
}, controller->lifetime());
i = _markdowns.emplace(key, std::move(controller)).first;
} else {
i->second->show(
std::move(prepared.content),
title,
std::move(options));
}
bindMarkdown(key, session, itemId);
i->second->activate();
}
bool Instance::showMarkdown(
const QString &path,
QVariant context) {
const auto target = ParseLocalMarkdownTarget(path);
const auto messageContext = ExtractMarkdownMessageContext(context);
const auto session = messageContext
? ResolveMarkdownSession(*messageContext)
: nullptr;
const auto itemId = messageContext
? messageContext->clickHandlerContext.itemId
: FullMsgId();
auto options = PrepareLocalMarkdownOptions(context);
if (!target.sourceName.isEmpty()) {
options.sourceName = target.sourceName;
options.sourcePath = target.key;
}
options.initialFragment = target.fragment;
auto i = _markdowns.find(target.key);
if (i == end(_markdowns)) {
if (auto controller = Markdown::TryOpenLocalFile(
_delegate,
target.path,
std::move(options))) {
controller->events() | rpl::on_next([=](Markdown::Event event) {
using Type = Markdown::Event::Type;
switch (event.type) {
case Type::Close:
_markdownBindings.remove(target.key);
_markdowns.take(target.key);
break;
case Type::Quit:
Shortcuts::Launch(Shortcuts::Command::Quit);
break;
case Type::Report:
break;
// Don't try opening markdown links inside markdown viewer,
// messenger-provided markdown files should know nothing
// about other local files and their paths.
//
//case Type::OpenFile:
// if (!showMarkdown(event.url, event.context)) {
// DEBUG_LOG(("Native Markdown IV: "
// "failed local markdown link: %1"
// ).arg(event.url));
// }
// break;
}
}, controller->lifetime());
i = _markdowns.emplace(target.key, std::move(controller)).first;
} else {
return false;
}
} else {
i->second->updateOptions(std::move(options));
}
if (session) {
bindMarkdown(target.key, not_null{ session }, itemId);
} else {
_markdownBindings.remove(target.key);
}
i->second->activate();
return true;
}
void Instance::requestFull(
not_null<Main::Session*> session,
const QString &id) {
if (!_tracking.contains(session)) {
return;
}
auto &requested = _fullRequested[session][id];
const auto last = requested.lastRequestedAt;
const auto now = crl::now();
if (last && (now - last) < kAllowPageReloadAfter) {
return;
}
requested.lastRequestedAt = now;
session->api().request(MTPmessages_GetWebPage(
MTP_string(id),
MTP_int(requested.hash)
)).done([=](const MTPmessages_WebPage &result) {
const auto processed = processReceivedPage(session, id, result);
if (processed.articleChanged
&& processed.page
&& processed.page->iv
&& _shown
&& _shownSession == session) {
_shown->update(processed.page->iv.get());
}
}).send();
}
Instance::ProcessReceivedPageResult Instance::processReceivedPage(
not_null<Main::Session*> session,
const QString &url,
const MTPmessages_WebPage &result) {
const auto &data = result.data();
const auto owner = &session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
auto &requested = _fullRequested[session][url];
auto processed = ProcessReceivedPageResult();
const auto &mtp = data.vwebpage();
mtp.match([&](const MTPDwebPageNotModified &data) {
Q_UNUSED(data);
processed.page = requested.page;
}, [&](const MTPDwebPage &data) {
const auto oldPage = requested.page;
const auto oldVersion = oldPage ? oldPage->version : 0;
const auto oldIvHash = (oldPage && oldPage->iv)
? oldPage->iv->hash()
: 0;
requested.hash = data.vhash().v;
processed.page = owner->processWebpage(data).get();
requested.page = processed.page;
processed.articleChanged = processed.page
&& processed.page->iv
&& (!oldPage
|| processed.page->version != oldVersion
|| processed.page->iv->hash() != oldIvHash);
}, [&](const auto &) {
processed.page = owner->processWebpage(mtp).get();
requested.page = processed.page;
});
return processed;
}
auto Instance::processReceivedRichMessage(
not_null<Main::Session*> session,
FullMsgId itemId,
const RichMessageGeneration &generation,
const MTPmessages_Messages &result)
-> ProcessReceivedRichMessageResult {
const auto owner = &session->data();
auto processed = ProcessReceivedRichMessageResult();
auto page = std::shared_ptr<const RichPage>();
result.match([&](const MTPDmessages_messagesNotModified &) {
LOG(("API Error: received messages.messagesNotModified!"));
}, [&](const auto &data) {
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
for (const auto &message : data.vmessages().v) {
if (message.type() != mtpc_message) {
continue;
}
const auto &parsed = message.c_message();
if (MsgId(parsed.vid().v) != itemId.msg) {
continue;
}
const auto richMessage = parsed.vrich_message();
page = richMessage ? ParseRichPage(session, *richMessage) : nullptr;
break;
}
});
const auto peer = owner->peer(itemId.peer);
result.match([&](const MTPDmessages_channelMessages &data) {
if (const auto channel = peer->asChannel()) {
channel->ptsReceived(data.vpts().v);
} else {
LOG(("App Error: received messages.channelMessages!"));
}
}, [](const auto &) {});
result.match([&](const MTPDmessages_messagesNotModified &) {
}, [&](const auto &data) {
peer->processTopics(data.vtopics());
});
const auto current = owner->message(itemId);
if (!current
|| !MatchesRichMessageGeneration(not_null{ current }, generation)) {
return processed;
}
if (page) {
current->setFullRichPage(page);
}
processed.page = std::move(page);
return processed;
}
void Instance::finishRichMessageRequest(
not_null<Main::Session*> session,
FullMsgId itemId,
uint64 token,
std::shared_ptr<const RichPage> page,
bool notifyCallbacks) {
const auto sessionIt = _richMessageRequested.find(session);
if (sessionIt == end(_richMessageRequested)) {
return;
}
const auto itemIt = sessionIt->second.find(itemId);
if (itemIt == end(sessionIt->second)) {
return;
}
if (itemIt->second.token != token) {
return;
}
auto callbacks = std::move(itemIt->second.callbacks);
itemIt->second.callbacks.clear();
itemIt->second.requestId = 0;
if (!notifyCallbacks) {
return;
}
for (auto &callback : callbacks) {
callback(page);
}
}
void Instance::processOpenChannel(const QString &context) {
if (!_shownSession) {
return;
}
const auto parsed = ParseNativeIvChannelContext(context);
if (const auto channelId = ChannelId(parsed.channelId)) {
const auto channel = _shownSession->data().channel(channelId);
if (channel->isLoaded()) {
if (const auto controller = _shownSession->tryResolveWindow(channel)) {
controller->showPeerHistory(channel);
_shown = nullptr;
}
} else if (const auto username = ResolveNativeIvChannelUsername(
channel->username(),
parsed.username); !username.isEmpty()) {
if (const auto controller = _shownSession->tryResolveWindow(channel)) {
controller->showPeerByLink({
.usernameOrId = username,
});
_shown = nullptr;
}
}
}
}
void Instance::processJoinChannel(const QString &context) {
if (!_shownSession) {
return;
}
const auto parsed = ParseNativeIvChannelContext(context);
if (const auto channelId = ChannelId(parsed.channelId)) {
const auto channel = _shownSession->data().channel(channelId);
_joining[_shownSession].emplace(channel);
if (channel->isLoaded()) {
_shownSession->api().joinChannel(channel);
} else if (const auto username = ResolveNativeIvChannelUsername(
channel->username(),
parsed.username); !username.isEmpty()) {
if (const auto controller = _shownSession->tryResolveWindow(channel)) {
controller->showPeerByLink({
.usernameOrId = username,
.joinChannel = true,
});
}
}
}
}
bool Instance::hasActiveWindow(not_null<Main::Session*> session) const {
return _shown && _shown->activeFor(session);
}
bool Instance::closeActive() {
if (_shown && _shown->active()) {
_shown = nullptr;
return true;
} else if (_tonSite && _tonSite->active()) {
_tonSite = nullptr;
return true;
}
for (auto &[key, controller] : _markdowns) {
if (controller->active()) {
_markdowns.take(key);
return true;
}
}
return false;
}
bool Instance::minimizeActive() {
if (_shown && _shown->active()) {
_shown->minimize();
return true;
} else if (_tonSite && _tonSite->active()) {
_tonSite->minimize();
return true;
}
return false;
}
void Instance::closeAll() {
_shown = nullptr;
_tonSite = nullptr;
}
bool PreferForUri(const QString &uri) {
const auto url = QUrl(uri);
const auto host = url.host().toLower();
const auto path = url.path().toLower();
return (host == u"telegra.ph"_q)
|| (host == u"te.legra.ph"_q)
|| (host == u"graph.org"_q)
|| (host == u"telegram.org"_q
&& (path.startsWith(u"/faq"_q)
|| path.startsWith(u"/privacy"_q)
|| path.startsWith(u"/blog"_q)));
}
} // namespace Iv