mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-15 21:57:10 +02:00
Support channel link / channel join.
This commit is contained in:
parent
f508ad5e75
commit
51d5b7bab6
12 changed files with 520 additions and 180 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ struct PeerByLinkInfo {
|
|||
QString startToken;
|
||||
ChatAdminRights startAdminRights;
|
||||
bool startAutoSubmit = false;
|
||||
bool joinChannel = false;
|
||||
QString botAppName;
|
||||
bool botAppForceConfirmation = false;
|
||||
QString attachBotUsername;
|
||||
|
|
Loading…
Add table
Reference in a new issue