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;