/* 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 "core/click_handler_types.h" #include "lang/lang_keys.h" #include "core/application.h" #include "core/local_url_handlers.h" #include "mainwidget.h" #include "mainwindow.h" #include "main/main_session.h" #include "ui/boxes/confirm_box.h" #include "ui/text/text_entity.h" #include "ui/toast/toast.h" #include "base/qthelp_regex.h" #include "base/qt/qt_key_modifiers.h" #include "storage/storage_account.h" #include "history/history.h" #include "history/view/history_view_element.h" #include "history/history_item.h" #include "data/data_user.h" #include "data/data_session.h" #include "window/window_session_controller.h" namespace { // Possible context owners: media viewer, profile, history widget. void SearchByHashtag(ClickContext context, const QString &tag) { const auto my = context.other.value(); const auto controller = my.sessionWindow.get(); if (!controller) { return; } if (controller->openedFolder().current()) { controller->closeFolder(); } controller->widget()->ui_hideSettingsAndLayer(anim::type::normal); Core::App().hideMediaView(); auto &data = controller->session().data(); const auto inPeer = my.peer ? my.peer : my.itemId ? data.message(my.itemId)->history()->peer.get() : nullptr; controller->content()->searchMessages( tag + ' ', (inPeer && !inPeer->isUser()) ? data.history(inPeer).get() : Dialogs::Key()); } } // namespace bool UrlRequiresConfirmation(const QUrl &url) { using namespace qthelp; return !regex_match( "(^|\\.)(" "telegram\\.(org|me|dog)" "|t\\.me" "|te\\.?legra\\.ph" "|graph\\.org" "|telesco\\.pe" ")$", url.host(), 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)) { return; } const auto open = [=] { UrlClickHandler::Open(url, context); }; if (url.startsWith(qstr("tg://"), Qt::CaseInsensitive) || url.startsWith(qstr("internal:"), Qt::CaseInsensitive)) { open(); } else { const auto parsedUrl = QUrl::fromUserInput(url); if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) { Core::App().hideMediaView(); const auto displayed = parsedUrl.isValid() ? parsedUrl.toDisplayString() : url; const auto displayUrl = !IsSuspicious(displayed) ? displayed : parsedUrl.isValid() ? QString::fromUtf8(parsedUrl.toEncoded()) : ShowEncoded(displayed); const auto my = context.value(); if (const auto controller = my.sessionWindow.get()) { controller->show( Ui::MakeConfirmBox({ .text = (tr::lng_open_this_link(tr::now) + qsl("\n\n") + displayUrl), .confirmed = [=](Fn hide) { hide(); open(); }, .confirmText = tr::lng_open_link(), }), Ui::LayerOption::KeepOther); } } else { open(); } } } void BotGameUrlClickHandler::onClick(ClickContext context) const { const auto url = Core::TryConvertUrlToLocal(this->url()); if (Core::InternalPassportLink(url)) { return; } const auto open = [=] { UrlClickHandler::Open(url, context.other); }; if (url.startsWith(qstr("tg://"), Qt::CaseInsensitive)) { open(); } else if (!_bot || _bot->isVerified() || _bot->session().local().isBotTrustedOpenGame(_bot->id)) { open(); } else { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { const auto callback = [=, bot = _bot](Fn close) { close(); bot->session().local().markBotTrustedOpenGame(bot->id); open(); }; controller->show(Ui::MakeConfirmBox({ .text = tr::lng_allow_bot_pass( tr::now, lt_bot_name, _bot->name), .confirmed = callback, .confirmText = tr::lng_allow_bot(), })); } } } auto HiddenUrlClickHandler::getTextEntity() const -> TextEntity { return { EntityType::CustomUrl, url() }; } QString MentionClickHandler::copyToClipboardContextItemText() const { return tr::lng_context_copy_mention(tr::now); } void MentionClickHandler::onClick(ClickContext context) const { const auto button = context.button; if (button == Qt::LeftButton || button == Qt::MiddleButton) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { using Info = Window::SessionNavigation::PeerByLinkInfo; controller->showPeerByLink(Info{ .usernameOrId = _tag.mid(1), .resolveType = Window::ResolveType::Mention, }); } } } auto MentionClickHandler::getTextEntity() const -> TextEntity { return { EntityType::Mention }; } void MentionNameClickHandler::onClick(ClickContext context) const { const auto button = context.button; if (button == Qt::LeftButton || button == Qt::MiddleButton) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { if (auto user = _session->data().userLoaded(_userId)) { controller->showPeerInfo(user); } } } } auto MentionNameClickHandler::getTextEntity() const -> TextEntity { const auto data = QString::number(_userId.bare) + '.' + QString::number(_accessHash); return { EntityType::MentionName, data }; } QString MentionNameClickHandler::tooltip() const { if (const auto user = _session->data().userLoaded(_userId)) { const auto name = user->name; if (name != _text) { return name; } } return QString(); } QString HashtagClickHandler::copyToClipboardContextItemText() const { return tr::lng_context_copy_hashtag(tr::now); } void HashtagClickHandler::onClick(ClickContext context) const { const auto button = context.button; if (button == Qt::LeftButton || button == Qt::MiddleButton) { SearchByHashtag(context, _tag); } } auto HashtagClickHandler::getTextEntity() const -> TextEntity { return { EntityType::Hashtag }; } QString CashtagClickHandler::copyToClipboardContextItemText() const { return tr::lng_context_copy_hashtag(tr::now); } void CashtagClickHandler::onClick(ClickContext context) const { const auto button = context.button; if (button == Qt::LeftButton || button == Qt::MiddleButton) { SearchByHashtag(context, _tag); } } auto CashtagClickHandler::getTextEntity() const -> TextEntity { return { EntityType::Cashtag }; } void BotCommandClickHandler::onClick(ClickContext context) const { const auto button = context.button; if (button != Qt::LeftButton && button != Qt::MiddleButton) { return; } const auto my = context.other.value(); if (const auto delegate = my.elementDelegate ? my.elementDelegate() : nullptr) { delegate->elementSendBotCommand(_cmd, my.itemId); } else if (const auto controller = my.sessionWindow.get()) { auto &data = controller->session().data(); const auto peer = my.peer ? my.peer : my.itemId ? data.message(my.itemId)->history()->peer.get() : nullptr; // Can't find context. if (!peer) { return; } controller->widget()->ui_hideSettingsAndLayer(anim::type::normal); Core::App().hideMediaView(); controller->content()->sendBotCommand({ .peer = peer, .command = _cmd, .context = my.itemId, .replyTo = 0, }); } } auto BotCommandClickHandler::getTextEntity() const -> TextEntity { return { EntityType::BotCommand }; } MonospaceClickHandler::MonospaceClickHandler( const QString &text, EntityType type) : _text(text) , _entity({ type }) { } void MonospaceClickHandler::onClick(ClickContext context) const { const auto button = context.button; if (button != Qt::LeftButton && button != Qt::MiddleButton) { return; } const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { auto &data = controller->session().data(); const auto item = data.message(my.itemId); const auto hasCopyRestriction = item && (!item->history()->peer->allowsForwarding() || item->forbidsForward()); const auto toastParent = Window::Show(controller).toastParent(); if (hasCopyRestriction) { Ui::Toast::Show( toastParent, item->history()->peer->isBroadcast() ? tr::lng_error_nocopy_channel(tr::now) : tr::lng_error_nocopy_group(tr::now)); return; } Ui::Toast::Show(toastParent, tr::lng_text_copied(tr::now)); } TextUtilities::SetClipboardText(TextForMimeData::Simple(_text.trimmed())); } auto MonospaceClickHandler::getTextEntity() const -> TextEntity { return _entity; } QString MonospaceClickHandler::url() const { return _text; }