diff --git a/Telegram/Resources/iv_html/page.css b/Telegram/Resources/iv_html/page.css index 591fab69b..f9b086a4d 100644 --- a/Telegram/Resources/iv_html/page.css +++ b/Telegram/Resources/iv_html/page.css @@ -35,7 +35,7 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover { border-radius: 50%; width: 32px; height: 32px; - box-shadow: 0 0 4px -2px var(--td-history-to-down-shadow); + box-shadow: 0 0 3px 0px var(--td-history-to-down-shadow); cursor: pointer; outline: none; z-index: 1000; @@ -44,6 +44,7 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover { display: flex; justify-content: center; align-items: center; + padding: 0px; } .fixed_button:hover { background-color: var(--td-history-to-down-bg-over); @@ -52,6 +53,8 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover { fill: none; position: relative; z-index: 1; + width: 24px; + height: 24px; } .fixed_button .ripple .inner { position: absolute; @@ -74,9 +77,10 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover { opacity: 0; } } -#top_menu svg { - width: 16px; - height: 16px; +@keyframes fadeIn { + to { + opacity: 1; + } } #top_menu circle { fill: var(--td-history-to-down-fg); @@ -89,13 +93,21 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover { right: 10px; } #top_back path, +#top_back line, #bottom_up path { stroke: var(--td-history-to-down-fg); - stroke-width: 2; +} +#top_back path, +#top_back line { + stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; } +#bottom_up path { + stroke-width: 1.4; +} #top_back:hover path, +#top_back:hover line, #bottom_up:hover path { stroke: var(--td-history-to-down-fg-over); } @@ -104,9 +116,6 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover { left: 10px; transition: left 200ms linear; } -#top_back svg { - transform: rotate(90deg); -} #top_back.hidden { left: -36px; } @@ -115,9 +124,6 @@ html.custom_scroll ::-webkit-scrollbar-thumb:hover { right: 10px; transition: bottom 200ms linear; } -#bottom_up svg { - transform: rotate(180deg); -} #bottom_up.hidden { bottom: -36px; } @@ -939,16 +945,23 @@ section.channel:first-child { } section.channel > a { display: block; - padding: 7px 18px; background: var(--td-box-divider-bg); } -section.channel > a:before { - content: var(--td-lng-group-call-join); +section.channel > a > div.join { color: var(--td-window-active-text-fg); font-weight: 500; - margin-left: 7px; + padding: 7px 18px; float: right; } +section.channel.joined > a > div.join { + display: none; +} +section.channel > a > div.join:hover { + text-decoration: underline; +} +section.channel > a > div.join span:before { + content: var(--td-lng-group-call-join); +} section.channel > a > h4 { font-family: 'Helvetica Neue'; font-size: 17px; @@ -959,6 +972,7 @@ section.channel > a > h4 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + padding: 7px 18px; } .iv-pullquote { @@ -976,3 +990,21 @@ section.channel > a > h4 { .iv-photo { background-size: 100%; } + +.toast { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--td-toast-bg); + color: var(--td-toast-fg); + padding: 10px 20px; + border-radius: 6px; + z-index: 9999; + opacity: 0; + animation: fadeIn 200ms linear forwards; +} +.toast.hiding { + opacity: 1; + animation: fadeOut 1000ms linear forwards; +} diff --git a/Telegram/Resources/iv_html/page.js b/Telegram/Resources/iv_html/page.js index 89aa8fc30..3f986998c 100644 --- a/Telegram/Resources/iv_html/page.js +++ b/Telegram/Resources/iv_html/page.js @@ -5,22 +5,36 @@ var IV = { } }, frameClickHandler: function(e) { - var target = e.target, href; - do { - if (target.tagName == 'SUMMARY') return; - if (target.tagName == 'DETAILS') return; - if (target.tagName == 'LABEL') return; - if (target.tagName == 'AUDIO') return; - if (target.tagName == 'A') break; - } while (target = target.parentNode); - if (target && target.hasAttribute('href')) { - var base_loc = document.createElement('A'); - base_loc.href = window.currentUrl; - if (base_loc.origin != target.origin || - base_loc.pathname != target.pathname || - base_loc.search != target.search) { - IV.notify({ event: 'link_click', url: target.href }); + var target = e.target; + var context = ''; + console.log('click', target); + while (target) { + if (target.tagName == 'AUDIO' || target.tagName == 'VIDEO') { + return; } + if (context === '' + && target.hasAttribute + && target.hasAttribute('data-context')) { + context = String(target.getAttribute('data-context')); + } + if (target.tagName == 'A') { + break; + } + target = target.parentNode; + } + if (!target || !target.hasAttribute('href')) { + return; + } + var base_loc = document.createElement('A'); + base_loc.href = window.currentUrl; + if (base_loc.origin != target.origin + || base_loc.pathname != target.pathname + || base_loc.search != target.search) { + IV.notify({ + event: 'link_click', + url: target.href, + context: context, + }); } e.preventDefault(); }, @@ -71,6 +85,16 @@ var IV = { document.getElementsByTagName('html')[0].style = styles; } }, + toggleChannelJoined: function (id, joined) { + const channels = document.getElementsByClassName('channel'); + const full = 'channel' + id; + for (var i = 0; i < channels.length; ++i) { + const channel = channels[i]; + if (String(channel.getAttribute('data-context')) === full) { + channel.classList.toggle('joined', joined); + } + } + }, slideshowSlide: function(el, next) { var dir = window.getComputedStyle(el, null).direction || 'ltr'; var marginProp = dir == 'rtl' ? 'marginRight' : 'marginLeft'; @@ -172,6 +196,19 @@ var IV = { IV.stopRipples(e.currentTarget); }); } + IV.notify({ event: 'ready' }); + }, + showTooltip: function (text) { + var toast = document.createElement('div'); + toast.classList.add('toast'); + toast.textContent = text; + document.body.appendChild(toast); + setTimeout(function () { + toast.classList.add('hiding'); + }, 2000); + setTimeout(function () { + document.body.removeChild(toast); + }, 3000); }, toTop: function () { document.getElementById('bottom_up').classList.add('hidden'); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index d0d3d8555..b5376fab2 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -3515,6 +3515,34 @@ void Session::webpageApplyFields( for (const auto &document : page->data().vdocuments().v) { processDocument(document); } + const auto process = [&]( + const MTPPageBlock &block, + const auto &self) -> void { + block.match([&](const MTPDpageBlockChannel &data) { + processChat(data.vchannel()); + }, [&](const MTPDpageBlockCover &data) { + self(data.vcover(), self); + }, [&](const MTPDpageBlockEmbedPost &data) { + for (const auto &block : data.vblocks().v) { + self(block, self); + } + }, [&](const MTPDpageBlockCollage &data) { + for (const auto &block : data.vitems().v) { + self(block, self); + } + }, [&](const MTPDpageBlockSlideshow &data) { + for (const auto &block : data.vitems().v) { + self(block, self); + } + }, [&](const MTPDpageBlockDetails &data) { + for (const auto &block : data.vblocks().v) { + self(block, self); + } + }, [](const auto &) {}); + }; + for (const auto &block : page->data().vblocks().v) { + process(block, process); + } } webpageApplyFields( page, diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index 5b46381fb..bab288d4f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -86,17 +86,47 @@ constexpr auto kMaxOriginalEntryLines = 8192; return result; } -[[nodiscard]] ClickHandlerPtr IvClickHandler(not_null webpage) { +[[nodiscard]] QString ExtractHash( + not_null webpage, + const TextWithEntities &text) { + const auto simplify = [](const QString &url) { + auto result = url.split('#')[0].toLower(); + if (result.endsWith('/')) { + result.chop(1); + } + const auto prefixes = { u"http://"_q, u"https://"_q }; + for (const auto &prefix : prefixes) { + if (result.startsWith(prefix)) { + result = result.mid(prefix.size()); + break; + } + } + return result; + }; + const auto simplified = simplify(webpage->url); + for (const auto &entity : text.entities) { + const auto link = (entity.type() == EntityType::Url) + ? text.text.mid(entity.offset(), entity.length()) + : (entity.type() == EntityType::CustomUrl) + ? entity.data() + : QString(); + if (simplify(link) == simplified) { + const auto i = link.indexOf('#'); + return (i > 0) ? link.mid(i + 1) : QString(); + } + } + return QString(); +} + +[[nodiscard]] ClickHandlerPtr IvClickHandler( + not_null webpage, + const TextWithEntities &text) { return std::make_shared([=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { if (const auto iv = webpage->iv.get()) { -#ifdef _DEBUG - const auto local = base::IsCtrlPressed(); -#else // _DEBUG - const auto local = false; -#endif // _DEBUG - Core::App().iv().show(controller->uiShow(), iv, local); + const auto hash = ExtractHash(webpage, text); + Core::App().iv().show(controller->uiShow(), iv, hash); return; } else { HiddenUrlClickHandler::Open(webpage->url, context.other); @@ -235,6 +265,7 @@ QSize WebPage::countOptimalSize() { const auto lineHeight = UnitedLineHeight(); if (!_openl && (!_data->url.isEmpty() || _sponsoredData)) { + const auto original = _parent->data()->originalText(); const auto previewOfHiddenUrl = [&] { if (_data->type == WebPageType::BotApp) { // Bot Web Apps always show confirmation on hidden urls. @@ -258,12 +289,11 @@ QSize WebPage::countOptimalSize() { return result; }; const auto simplified = simplify(_data->url); - const auto full = _parent->data()->originalText(); - for (const auto &entity : full.entities) { + for (const auto &entity : original.entities) { if (entity.type() != EntityType::Url) { continue; } - const auto link = full.text.mid( + const auto link = original.text.mid( entity.offset(), entity.length()); if (simplify(link) == simplified) { @@ -272,8 +302,10 @@ QSize WebPage::countOptimalSize() { } return true; }(); - _openl = _data->iv ? IvClickHandler(_data) : (previewOfHiddenUrl - || UrlClickHandler::IsSuspicious(_data->url)) + _openl = _data->iv + ? IvClickHandler(_data, original) + : (previewOfHiddenUrl || UrlClickHandler::IsSuspicious( + _data->url)) ? std::make_shared(_data->url) : std::make_shared(_data->url, true); if (_data->document diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index 6a775466d..592cd3be9 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -58,6 +58,8 @@ namespace { { "history-to-down-bg-over", &st::historyToDownBgOver }, { "history-to-down-bg-ripple", &st::historyToDownBgRipple }, { "history-to-down-shadow", &st::historyToDownShadow }, + { "toast-bg", &st::toastBg }, + { "toast-fg", &st::toastFg }, }; static const auto phrases = base::flat_map>{ { "group-call-join", tr::lng_group_call_join }, @@ -125,30 +127,125 @@ namespace { .replace('\'', "\\\'"); } +[[nodiscard]] QByteArray WrapPage( + const Prepared &page, + const QByteArray &initScript) { +#ifdef Q_OS_MAC + const auto classAttribute = ""_q; +#else // Q_OS_MAC + const auto classAttribute = " class=\"custom_scroll\""_q; +#endif // Q_OS_MAC + + const auto js = QByteArray() + + (page.hasCode ? "IV.initPreBlocks();" : "") + + (page.hasEmbeds ? "IV.initEmbedBlocks();" : "") + + "IV.init();" + + initScript; + + const auto contentAttributes = page.rtl + ? " dir=\"rtl\" class=\"rtl\""_q + : QByteArray(); + + return R"( + + + + + + + + + + + + + + + "_q + page.content + R"( + + + +)"_q; +} + } // namespace + Controller::Controller() : _updateStyles([=] { const auto str = EscapeForScriptString(ComputeStyles()); if (_webview) { - _webview->eval("IV.updateStyles(\"" + str + "\");"); + _webview->eval("IV.updateStyles('" + str + "');"); } }) { } Controller::~Controller() { + _ready = false; _webview = nullptr; _title = nullptr; _window = nullptr; } -void Controller::show(const QString &dataPath, Prepared page) { +void Controller::show( + const QString &dataPath, + Prepared page, + base::flat_map> inChannelValues) { createWindow(); + const auto js = fillInChannelValuesScript(std::move(inChannelValues)); + _titleText.setText(st::ivTitle.style, page.title); InvokeQueued(_container, [=, page = std::move(page)]() mutable { - showInWindow(dataPath, std::move(page)); + showInWindow(dataPath, std::move(page), js); + if (!_webview) { + return; + } }); } +QByteArray Controller::fillInChannelValuesScript( + base::flat_map> inChannelValues) { + auto result = QByteArray(); + for (auto &[id, in] : inChannelValues) { + std::move(in) | rpl::start_with_next([=](bool in) { + if (_ready) { + _webview->eval(toggleInChannelScript(id, in)); + } else { + _inChannelChanged[id] = in; + } + }, _lifetime); + } + for (const auto &[id, in] : base::take(_inChannelChanged)) { + result += toggleInChannelScript(id, in); + } + return result; +} + +QByteArray Controller::toggleInChannelScript( + const QByteArray &id, + bool in) const { + const auto value = in ? "true" : "false"; + return "IV.toggleChannelJoined('" + id + "', " + value + ");"; +} + void Controller::updateTitleGeometry() { _title->setGeometry(0, 0, _window->width(), st::ivTitle.height); } @@ -242,7 +339,10 @@ void Controller::createWindow() { window->show(); } -void Controller::showInWindow(const QString &dataPath, Prepared page) { +void Controller::showInWindow( + const QString &dataPath, + Prepared page, + const QByteArray &initScript) { Expects(_container != nullptr); const auto window = _window.get(); @@ -255,10 +355,11 @@ void Controller::showInWindow(const QString &dataPath, Prepared page) { const auto raw = _webview.get(); window->lifetime().add([=] { + _ready = false; _webview = nullptr; }); if (!raw->widget()) { - _events.fire(Event::Close); + _events.fire({ Event::Type::Close }); return; } window->events( @@ -291,20 +392,24 @@ void Controller::showInWindow(const QString &dataPath, Prepared page) { if (event == u"keydown"_q) { const auto key = object.value("key").toString(); const auto modifier = object.value("modifier").toString(); - const auto ctrl = Platform::IsMac() ? u"cmd"_q : u"ctrl"_q; - if (key == u"escape"_q) { - escape(); - } else if (key == u"w"_q && modifier == ctrl) { - close(); - } else if (key == u"m"_q && modifier == ctrl) { - minimize(); - } else if (key == u"q"_q && modifier == ctrl) { - quit(); - } + processKey(key, modifier); } else if (event == u"mouseenter"_q) { window->overrideSystemButtonOver({}); } else if (event == u"mouseup"_q) { window->overrideSystemButtonDown({}); + } else if (event == u"link_click"_q) { + const auto url = object.value("url").toString(); + const auto context = object.value("context").toString(); + processLink(url, context); + } else if (event == u"ready"_q) { + _ready = true; + auto script = QByteArray(); + for (const auto &[id, in] : base::take(_inChannelChanged)) { + script += toggleInChannelScript(id, in); + } + if (!script.isEmpty()) { + _webview->eval(script); + } } }); }); @@ -323,11 +428,6 @@ void Controller::showInWindow(const QString &dataPath, Prepared page) { }; const auto id = std::string_view(request.id).substr(3); if (id == "page.html") { - const auto i = page.html.indexOf("= 0); - const auto colored = page.html.mid(0, i + 5) - + " style=\"" + EscapeForAttribute(ComputeStyles()) + "\"" - + page.html.mid(i + 5); if (!_subscribedToColors) { _subscribedToColors = true; @@ -338,7 +438,7 @@ void Controller::showInWindow(const QString &dataPath, Prepared page) { _updateStyles.call(); }, _webview->lifetime()); } - return finishWith(colored, "text/html"); + return finishWith(WrapPage(page, initScript), "text/html"); } const auto css = id.ends_with(".css"); const auto js = !css && id.ends_with(".js"); @@ -357,15 +457,52 @@ void Controller::showInWindow(const QString &dataPath, Prepared page) { return Webview::DataResult::Failed; }); - raw->init(R"( -)"); + raw->init(R"()"); raw->navigateToData("iv/page.html"); } +void Controller::processKey(const QString &key, const QString &modifier) { + const auto ctrl = Platform::IsMac() ? u"cmd"_q : u"ctrl"_q; + if (key == u"escape"_q) { + escape(); + } else if (key == u"w"_q && modifier == ctrl) { + close(); + } else if (key == u"m"_q && modifier == ctrl) { + minimize(); + } else if (key == u"q"_q && modifier == ctrl) { + quit(); + } +} + +void Controller::processLink(const QString &url, const QString &context) { + const auto channelPrefix = u"channel"_q; + const auto joinPrefix = u"join_link"_q; + if (context.startsWith(channelPrefix)) { + _events.fire({ + Event::Type::OpenChannel, + context.mid(channelPrefix.size()), + }); + } else if (context.startsWith(joinPrefix)) { + _events.fire({ + Event::Type::JoinChannel, + context.mid(joinPrefix.size()), + }); + } +} + bool Controller::active() const { return _window && _window->isActiveWindow(); } +void Controller::showJoinedTooltip() { + if (_webview) { + _webview->eval("IV.showTooltip('" + + EscapeForScriptString( + tr::lng_action_you_joined(tr::now).toUtf8()) + + "');"); + } +} + void Controller::minimize() { if (_window) { _window->setWindowState(_window->windowState() @@ -378,11 +515,11 @@ void Controller::escape() { } void Controller::close() { - _events.fire(Event::Close); + _events.fire({ Event::Type::Close }); } void Controller::quit() { - _events.fire(Event::Quit); + _events.fire({ Event::Type::Quit }); } rpl::lifetime &Controller::lifetime() { diff --git a/Telegram/SourceFiles/iv/iv_controller.h b/Telegram/SourceFiles/iv/iv_controller.h index 1170974e1..5b565d285 100644 --- a/Telegram/SourceFiles/iv/iv_controller.h +++ b/Telegram/SourceFiles/iv/iv_controller.h @@ -32,13 +32,23 @@ public: Controller(); ~Controller(); - enum class Event { - Close, - Quit, + struct Event { + enum class Type { + Close, + Quit, + OpenChannel, + JoinChannel, + }; + Type type = Type::Close; + QString context; }; - void show(const QString &dataPath, Prepared page); + void show( + const QString &dataPath, + Prepared page, + base::flat_map> inChannelValues); [[nodiscard]] bool active() const; + void showJoinedTooltip(); void minimize(); [[nodiscard]] rpl::producer dataRequests() const { @@ -55,7 +65,18 @@ private: void createWindow(); void updateTitleGeometry(); void paintTitle(Painter &p, QRect clip); - void showInWindow(const QString &dataPath, Prepared page); + void showInWindow( + const QString &dataPath, + Prepared page, + const QByteArray &initScript); + [[nodiscard]] QByteArray fillInChannelValuesScript( + base::flat_map> inChannelValues); + [[nodiscard]] QByteArray toggleInChannelScript( + const QByteArray &id, + bool in) const; + + void processKey(const QString &key, const QString &modifier); + void processLink(const QString &url, const QString &context); void escape(); void close(); @@ -70,8 +91,10 @@ private: std::unique_ptr _webview; rpl::event_stream _dataRequests; rpl::event_stream _events; + base::flat_map _inChannelChanged; SingleQueuedInvokation _updateStyles; bool _subscribedToColors = false; + bool _ready = false; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/iv/iv_data.h b/Telegram/SourceFiles/iv/iv_data.h index 3c2d04980..97acdd753 100644 --- a/Telegram/SourceFiles/iv/iv_data.h +++ b/Telegram/SourceFiles/iv/iv_data.h @@ -17,9 +17,13 @@ struct Options { struct Prepared { QString title; - QByteArray html; + QByteArray content; std::vector resources; base::flat_map embeds; + base::flat_set channelIds; + bool rtl = false; + bool hasCode = false; + bool hasEmbeds = false; }; struct Geo { diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index 4a1accd7e..b597ddf95 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -7,13 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "iv/iv_instance.h" +#include "apiwrap.h" +#include "core/application.h" #include "core/file_utilities.h" #include "core/shortcuts.h" +#include "data/data_changes.h" +#include "data/data_channel.h" #include "data/data_cloud_file.h" #include "data/data_document.h" #include "data/data_file_origin.h" #include "data/data_photo_media.h" #include "data/data_session.h" +#include "info/profile/info_profile_values.h" #include "iv/iv_controller.h" #include "iv/iv_data.h" #include "main/main_account.h" @@ -26,6 +31,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "webview/webview_data_stream_memory.h" #include "webview/webview_interface.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "window/window_session_controller_link_info.h" namespace Iv { namespace { @@ -66,6 +74,7 @@ public: [[nodiscard]] bool activeFor(not_null session) const; [[nodiscard]] bool active() const; + void showJoinedTooltip(); void minimize(); [[nodiscard]] rpl::producer events() const { @@ -123,6 +132,7 @@ private: void streamMap(QString params, Webview::DataRequest request); void sendEmbed(QByteArray hash, Webview::DataRequest request); + void fillChannelJoinedValues(const Prepared &result); void requestDone( Webview::DataRequest request, QByteArray bytes, @@ -136,6 +146,7 @@ private: QString _id; std::unique_ptr _controller; base::flat_map _files; + base::flat_map> _inChannelValues; QString _localBase; base::flat_map _embeds; @@ -162,6 +173,7 @@ Shown::Shown( data->prepare({ .saveToFolder = base }, [=](Prepared result) { crl::on_main(weak, [=, result = std::move(result)]() mutable { _embeds = std::move(result.embeds); + fillChannelJoinedValues(result); if (!base.isEmpty()) { _localBase = base; showLocal(std::move(result)); @@ -172,6 +184,22 @@ Shown::Shown( }); } +void Shown::fillChannelJoinedValues(const Prepared &result) { + for (const auto &id : result.channelIds) { + const auto channelId = ChannelId(id.toLongLong()); + const auto channel = _session->data().channel(channelId); + if (!channel->isLoaded() && !channel->username().isEmpty()) { + channel->session().api().request(MTPcontacts_ResolveUsername( + MTP_string(channel->username()) + )).done([=](const MTPcontacts_ResolvedPeer &result) { + channel->owner().processUsers(result.data().vusers()); + channel->owner().processChats(result.data().vchats()); + }).send(); + } + _inChannelValues[id] = Info::Profile::AmInChannelValue(channel); + } +} + void Shown::showLocal(Prepared result) { showProgress(0); @@ -179,7 +207,7 @@ void Shown::showLocal(Prepared result) { QDir().mkpath(_localBase); _resources = std::move(result.resources); - writeLocal(localRoot(), result.html); + writeLocal(localRoot(), result.content); } void Shown::showProgress(int index) { @@ -392,7 +420,10 @@ void Shown::showWindowed(Prepared result) { }, _controller->lifetime()); const auto domain = &_session->domain(); - _controller->show(domain->local().webviewDataPath(), std::move(result)); + _controller->show( + domain->local().webviewDataPath(), + std::move(result), + base::duplicate(_inChannelValues)); } void Shown::streamPhoto(PhotoId photoId, Webview::DataRequest request) { @@ -651,6 +682,12 @@ bool Shown::active() const { return _controller && _controller->active(); } +void Shown::showJoinedTooltip() { + if (_controller) { + _controller->showJoinedTooltip(); + } +} + void Shown::minimize() { if (_controller) { _controller->minimize(); @@ -670,11 +707,34 @@ void Instance::show( return; } _shown = std::make_unique(show, data, local); + _shownSession = session; _shown->events() | rpl::start_with_next([=](Controller::Event event) { - if (event == Controller::Event::Close) { + using Type = Controller::Event::Type; + switch (event.type) { + case Type::Close: _shown = nullptr; - } else if (event == Controller::Event::Quit) { + break; + case Type::Quit: Shortcuts::Launch(Shortcuts::Command::Quit); + break; + case Type::OpenChannel: + processOpenChannel(event.context); + break; + case Type::JoinChannel: + processJoinChannel(event.context); + break; + } + }, _shown->lifetime()); + + session->changes().peerUpdates( + ::Data::PeerUpdate::Flag::ChannelAmIn + ) | rpl::start_with_next([=](const ::Data::PeerUpdate &update) { + if (const auto channel = update.peer->asChannel()) { + if (channel->amIn()) { + if (_joining.remove(not_null(channel))) { + _shown->showJoinedTooltip(); + } + } } }, _shown->lifetime()); @@ -682,6 +742,16 @@ void Instance::show( _tracking.emplace(session); session->lifetime().add([=] { _tracking.remove(session); + for (auto i = begin(_joining); i != end(_joining);) { + if (&(*i)->session() == session) { + i = _joining.erase(i); + } else { + ++i; + } + } + if (_shownSession == session) { + _shownSession = nullptr; + } if (_shown && _shown->showingFrom(session)) { _shown = nullptr; } @@ -689,6 +759,52 @@ void Instance::show( } } +void Instance::processOpenChannel(const QString &context) { + if (!_shownSession) { + return; + } else if (const auto channelId = ChannelId(context.toLongLong())) { + const auto channel = _shownSession->data().channel(channelId); + if (channel->isLoaded()) { + if (const auto window = Core::App().windowFor(channel)) { + if (const auto controller = window->sessionController()) { + controller->showPeerHistory(channel); + _shown = nullptr; + } + } + } else if (!channel->username().isEmpty()) { + if (const auto window = Core::App().windowFor(channel)) { + if (const auto controller = window->sessionController()) { + controller->showPeerByLink({ + .usernameOrId = channel->username(), + }); + _shown = nullptr; + } + } + } + } +} + +void Instance::processJoinChannel(const QString &context) { + if (!_shownSession) { + return; + } else if (const auto channelId = ChannelId(context.toLongLong())) { + const auto channel = _shownSession->data().channel(channelId); + _joining.emplace(channel); + if (channel->isLoaded()) { + _shownSession->api().joinChannel(channel); + } else if (!channel->username().isEmpty()) { + if (const auto window = Core::App().windowFor(channel)) { + if (const auto controller = window->sessionController()) { + controller->showPeerByLink({ + .usernameOrId = channel->username(), + .joinChannel = true, + }); + } + } + } + } +} + bool Instance::hasActiveWindow(not_null session) const { return _shown && _shown->activeFor(session); } diff --git a/Telegram/SourceFiles/iv/iv_instance.h b/Telegram/SourceFiles/iv/iv_instance.h index 566876303..87c061f21 100644 --- a/Telegram/SourceFiles/iv/iv_instance.h +++ b/Telegram/SourceFiles/iv/iv_instance.h @@ -38,8 +38,13 @@ public: [[nodiscard]] rpl::lifetime &lifetime(); private: + void processOpenChannel(const QString &context); + void processJoinChannel(const QString &context); + std::unique_ptr _shown; + Main::Session *_shownSession = nullptr; base::flat_set> _tracking; + base::flat_set> _joining; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/iv/iv_prepare.cpp b/Telegram/SourceFiles/iv/iv_prepare.cpp index a1b8749ef..58bf05d4a 100644 --- a/Telegram/SourceFiles/iv/iv_prepare.cpp +++ b/Telegram/SourceFiles/iv/iv_prepare.cpp @@ -47,14 +47,6 @@ private: void process(const MTPPhoto &photo); void process(const MTPDocument &document); - [[nodiscard]] QByteArray prepare(QByteArray body); - - [[nodiscard]] QByteArray html( - const QByteArray &head, - const QByteArray &body); - - [[nodiscard]] QByteArray page(const MTPDpage &data); - template [[nodiscard]] QByteArray list(const MTPVector &data); @@ -143,9 +135,6 @@ private: base::flat_map _photosById; base::flat_map _documentsById; - bool _hasCode = false; - bool _hasEmbeds = false; - }; [[nodiscard]] bool IsVoidElement(const QByteArray &name) { @@ -169,11 +158,11 @@ private: } Parser::Parser(const Source &source, const Options &options) -: _options(options) -, _rtl(source.page.data().is_rtl()) { +: _options(options) { process(source); _result.title = source.title; - _result.html = prepare(page(source.page.data())); + _result.rtl = source.page.data().is_rtl(); + _result.content = list(source.page.data().vblocks()); } Prepared Parser::result() { @@ -260,7 +249,7 @@ QByteArray Parser::block(const MTPDpageBlockPreformatted &data) { if (!language.isEmpty()) { list.push_back({ "data-language", language }); list.push_back({ "class", "lang-" + language }); - _hasCode = true; + _result.hasCode = true; } return tag("pre", list, rich(data.vtext())); } @@ -270,7 +259,7 @@ QByteArray Parser::block(const MTPDpageBlockFooter &data) { } QByteArray Parser::block(const MTPDpageBlockDivider &data) { - return tag("hr", { {"class", "iv-divider" } }); + return tag("hr", { { "class", "iv-divider" } }); } QByteArray Parser::block(const MTPDpageBlockAnchor &data) { @@ -393,7 +382,7 @@ QByteArray Parser::block(const MTPDpageBlockCover &data) { } QByteArray Parser::block(const MTPDpageBlockEmbed &data) { - _hasEmbeds = true; + _result.hasEmbeds = true; auto eclass = data.is_full_width() ? QByteArray() : "nowide"; auto width = QByteArray(); auto height = QByteArray(); @@ -519,6 +508,9 @@ QByteArray Parser::block(const MTPDpageBlockSlideshow &data) { QByteArray Parser::block(const MTPDpageBlockChannel &data) { auto name = QByteArray(); auto username = QByteArray(); + auto id = data.vchannel().match([](const auto &data) { + return QByteArray::number(data.vid().v); + }); data.vchannel().match([&](const MTPDchannel &data) { if (const auto has = data.vusername()) { username = utf(*has); @@ -528,15 +520,23 @@ QByteArray Parser::block(const MTPDpageBlockChannel &data) { name = utf(data.vtitle()); }, [](const auto &) { }); - auto result = tag("h4", name); - if (!username.isEmpty()) { - const auto link = "https://t.me/" + username; - result = tag( - "a", - { { "href", link }, { "target", "_blank" } }, - result); - } - return tag("section", { { "class", "channel" } }, result); + auto result = tag( + "div", + { { "class", "join" }, { "data-context", "join_link" + id } }, + tag("span") + ) + tag("h4", name); + const auto link = username.isEmpty() + ? "javascript:alert('Channel Link');" + : "https://t.me/" + username; + result = tag( + "a", + { { "href", link }, { "data-context", "channel" + id } }, + result); + _result.channelIds.emplace(id); + return tag("section", { + { "class", "channel joined" }, + { "data-context", "channel" + id }, + }, result); } QByteArray Parser::block(const MTPDpageBlockAudio &data) { @@ -972,83 +972,6 @@ QByteArray Parser::resource(QByteArray id) { return toFolder ? id : ('/' + id); } -QByteArray Parser::page(const MTPDpage &data) { - const auto html = list(data.vblocks()); - if (html.isEmpty()) { - return html; - } - auto attributes = Attributes(); - if (_rtl) { - attributes.push_back({ "dir", "rtl" }); - attributes.push_back({ "class", "rtl" }); - } - return tag("article", attributes, html); -} - -QByteArray Parser::prepare(QByteArray body) { - auto head = QByteArray(); - auto js = QByteArray(); - if (body.isEmpty()) { - body = tag( - "section", - { { "class", "message" } }, - tag("aside", "Failed." + tag("cite", "Failed."))); - } - if (_hasCode) { - head += R"( - - -)"_q; - js += "IV.initPreBlocks();"; - } - if (_hasEmbeds) { - js += "IV.initEmbedBlocks();"; - } - body += tag("script", js + "IV.init();"); - return html(head, body); -} - -QByteArray Parser::html(const QByteArray &head, const QByteArray &body) { -#ifdef Q_OS_MAC - const auto classAttribute = ""_q; -#else // Q_OS_MAC - const auto classAttribute = " class=\"custom_scroll\""_q; -#endif // Q_OS_MAC - - return R"( - - - - - - - - )"_q + head + R"( - - - - - -)"_q + body + R"( - - -)"_q; -} - } // namespace Prepared Prepare(const Source &source, const Options &options) { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 15ea551e8..031946f0a 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -314,6 +314,8 @@ void SessionNavigation::showPeerByLink(const PeerByLinkInfo &info) { peer, [=](bool) { showPeerByLinkResolved(peer, info); }, true); + } else if (info.joinChannel && peer->isChannel()) { + peer->session().api().joinChannel(peer->asChannel()); } else { showPeerByLinkResolved(peer, info); } diff --git a/Telegram/SourceFiles/window/window_session_controller_link_info.h b/Telegram/SourceFiles/window/window_session_controller_link_info.h index 927185e5a..5cf6dce7e 100644 --- a/Telegram/SourceFiles/window/window_session_controller_link_info.h +++ b/Telegram/SourceFiles/window/window_session_controller_link_info.h @@ -38,6 +38,7 @@ struct PeerByLinkInfo { QString startToken; ChatAdminRights startAdminRights; bool startAutoSubmit = false; + bool joinChannel = false; QString botAppName; bool botAppForceConfirmation = false; QString attachBotUsername;