Support channel link / channel join.

This commit is contained in:
John Preston 2023-12-04 23:06:44 +04:00
parent f508ad5e75
commit 51d5b7bab6
12 changed files with 520 additions and 180 deletions

View file

@ -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;
}

View file

@ -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');

View file

@ -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,

View file

@ -86,17 +86,47 @@ constexpr auto kMaxOriginalEntryLines = 8192;
return result;
}
[[nodiscard]] ClickHandlerPtr IvClickHandler(not_null<WebPageData*> webpage) {
[[nodiscard]] QString ExtractHash(
not_null<WebPageData*> 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<WebPageData*> webpage,
const TextWithEntities &text) {
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
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<HiddenUrlClickHandler>(_data->url)
: std::make_shared<UrlClickHandler>(_data->url, true);
if (_data->document

View file

@ -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<QByteArray, tr::phrase<>>{
{ "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"(<!DOCTYPE html>
<html)"_q
+ classAttribute
+ R"("" style=")"
+ EscapeForAttribute(ComputeStyles())
+ R"(">
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="/iv/page.js"></script>
<script src="/iv/highlight.js"></script>
<link rel="stylesheet" href="/iv/page.css" />
<link rel="stylesheet" href="/iv/highlight.css">
</head>
<body>
<button class="fixed_button hidden" id="top_back" onclick="IV.back();">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<line x1="5.37464142" y1="12" x2="18.5" y2="12"></line>
<path d="M11.5,18.3 L5.27277119,12.0707223 C5.23375754,12.0316493 5.23375754,11.9683507 5.27277119,11.9292777 L11.5,5.7 L11.5,5.7"></path>
</svg>
</button>
<button class="fixed_button" id="top_menu" onclick="IV.menu();">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="17.4" r="1.7"></circle>
<circle cx="12" cy="12" r="1.7"></circle>
<circle cx="12" cy="6.6" r="1.7"></circle>
</svg>
</button>
<button class="fixed_button hidden" id="bottom_up" onclick="IV.toTop();">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M14.9972363,18 L9.13865768,12.1414214 C9.06055283,12.0633165 9.06055283,11.9366835 9.13865768,11.8585786 L14.9972363,6 L14.9972363,6" transform="translate(11.997236, 12.000000) scale(-1, -1) rotate(-90.000000) translate(-11.997236, -12.000000) "></path>
</svg>
</button>
<article)"_q + contentAttributes + ">"_q + page.content + R"(</article>
<script>)"_q + js + R"(</script>
</body>
</html>
)"_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<QByteArray, rpl::producer<bool>> 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<QByteArray, rpl::producer<bool>> 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("<html"_q);
Assert(i >= 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() {

View file

@ -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<QByteArray, rpl::producer<bool>> inChannelValues);
[[nodiscard]] bool active() const;
void showJoinedTooltip();
void minimize();
[[nodiscard]] rpl::producer<Webview::DataRequest> 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<QByteArray, rpl::producer<bool>> 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::Window> _webview;
rpl::event_stream<Webview::DataRequest> _dataRequests;
rpl::event_stream<Event> _events;
base::flat_map<QByteArray, bool> _inChannelChanged;
SingleQueuedInvokation _updateStyles;
bool _subscribedToColors = false;
bool _ready = false;
rpl::lifetime _lifetime;

View file

@ -17,9 +17,13 @@ struct Options {
struct Prepared {
QString title;
QByteArray html;
QByteArray content;
std::vector<QByteArray> resources;
base::flat_map<QByteArray, QByteArray> embeds;
base::flat_set<QByteArray> channelIds;
bool rtl = false;
bool hasCode = false;
bool hasEmbeds = false;
};
struct Geo {

View file

@ -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<Main::Session*> session) const;
[[nodiscard]] bool active() const;
void showJoinedTooltip();
void minimize();
[[nodiscard]] rpl::producer<Controller::Event> 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> _controller;
base::flat_map<DocumentId, FileLoad> _files;
base::flat_map<QByteArray, rpl::producer<bool>> _inChannelValues;
QString _localBase;
base::flat_map<QByteArray, QByteArray> _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<Shown>(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<Main::Session*> session) const {
return _shown && _shown->activeFor(session);
}

View file

@ -38,8 +38,13 @@ public:
[[nodiscard]] rpl::lifetime &lifetime();
private:
void processOpenChannel(const QString &context);
void processJoinChannel(const QString &context);
std::unique_ptr<Shown> _shown;
Main::Session *_shownSession = nullptr;
base::flat_set<not_null<Main::Session*>> _tracking;
base::flat_set<not_null<ChannelData*>> _joining;
rpl::lifetime _lifetime;

View file

@ -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 <typename Inner>
[[nodiscard]] QByteArray list(const MTPVector<Inner> &data);
@ -143,9 +135,6 @@ private:
base::flat_map<uint64, Photo> _photosById;
base::flat_map<uint64, Document> _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"(
<link rel="stylesheet" href=")" + resource("iv/highlight.css") + R"(">
<script src=")" + resource("iv/highlight.js") + R"("></script>
)"_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"(<!DOCTYPE html>
<html)"_q + classAttribute + R"(">
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src=")" + resource("iv/page.js") + R"("></script>
<link rel="stylesheet" href=")" + resource("iv/page.css") + R"(" />
)"_q + head + R"(
</head>
<body>
<button class="fixed_button hidden" id="top_back" onclick="IV.back();">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M17 13L12 18L7 13M12 6L12 17"></path>
</svg>
</button>
<button class="fixed_button" id="top_menu" onclick="IV.menu();">
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="2.5" r="1.6"></circle>
<circle cx="8" cy="8" r="1.6"></circle>
<circle cx="8" cy="13.5" r="1.6"></circle>
</svg>
</button>
<button class="fixed_button hidden" id="bottom_up" onclick="IV.toTop();">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M17 13L12 18L7 13M12 6L12 17"></path>
</svg>
</button>
)"_q + body + R"(
</body>
</html>
)"_q;
}
} // namespace
Prepared Prepare(const Source &source, const Options &options) {

View file

@ -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);
}

View file

@ -38,6 +38,7 @@ struct PeerByLinkInfo {
QString startToken;
ChatAdminRights startAdminRights;
bool startAutoSubmit = false;
bool joinChannel = false;
QString botAppName;
bool botAppForceConfirmation = false;
QString attachBotUsername;