Load full webpage and update in IV.

This commit is contained in:
John Preston 2024-03-11 16:59:11 +04:00
parent 0a87dbea68
commit 315859bf7b
10 changed files with 199 additions and 38 deletions

File diff suppressed because one or more lines are too long

View File

@ -335,39 +335,82 @@ var IV = {
}
IV.pending = [index, hash];
if (!IV.cache[index]) {
IV.cache[index] = { loading: true };
let xhr = new XMLHttpRequest();
xhr.onload = function () {
IV.cache[index].loading = false;
IV.cache[index].content = xhr.responseText;
if (IV.pending && IV.pending[0] == index) {
IV.navigateToLoaded(index, IV.pending[1]);
}
}
xhr.open('GET', 'page' + index + '.json');
xhr.send();
IV.loadPage(index);
} else if (IV.cache[index].dom) {
IV.navigateToDOM(index, hash);
} else if (IV.cache[index].content) {
IV.navigateToLoaded(index, hash);
}
},
applyUpdatedContent: function (index) {
if (IV.index != index) {
IV.cache[index].contentUpdated = (IV.cache[index].dom !== undefined);
return;
}
var data = JSON.parse(IV.cache[index].content);
var article = function (el) {
return el.getElementsByTagName('article')[0];
};
var from = article(IV.findPageScroll());
var to = article(IV.makeScrolledContent(data.html));
morphdom(from, to, {
onBeforeElUpdated: function (fromEl, toEl) {
if (fromEl.classList.contains('loaded')) {
toEl.classList.add('loaded');
}
return !fromEl.isEqualNode(toEl);
}
});
IV.initMedia();
eval(data.js);
},
loadPage: function (index) {
if (!IV.cache[index]) {
IV.cache[index] = {};
}
IV.cache[index].loading = true;
let xhr = new XMLHttpRequest();
xhr.onload = function () {
IV.cache[index].loading = false;
IV.cache[index].content = xhr.responseText;
IV.applyUpdatedContent(index);
if (IV.pending && IV.pending[0] == index) {
IV.navigateToLoaded(index, IV.pending[1]);
}
if (IV.cache[index].reloadPending) {
IV.cache[index].reloadPending = false;
IV.reloadPage(index);
}
}
xhr.open('GET', 'page' + index + '.json');
xhr.send();
},
reloadPage: function (index) {
if (IV.cache[index] && IV.cache[index].loading) {
IV.cache[index].reloadPending = true;
return;
}
IV.loadPage(index);
},
makeScrolledContent: function (html) {
var result = document.createElement('div');
result.className = 'page-scroll';
result.tabIndex = '-1';
result.innerHTML = '<div class="page-slide"><article>'
+ html
+ '</article></div>';
result.onscroll = IV.frameScrolled;
return result;
},
navigateToLoaded: function (index, hash) {
if (IV.cache[index].dom) {
IV.navigateToDOM(index, hash);
} else {
var data = JSON.parse(IV.cache[index].content);
var el = document.createElement('div');
el.className = 'page-scroll';
el.tabIndex = '-1';
el.innerHTML = '<div class="page-slide"><article>'
+ data.html
+ '</article></div>';
el.onscroll = IV.frameScrolled;
IV.cache[index].dom = el;
IV.cache[index].dom = IV.makeScrolledContent(data.html);
IV.navigateToDOM(index, hash);
eval(data.js);
@ -417,6 +460,14 @@ var IV = {
was.parentNode.appendChild(now);
if (scroll !== undefined) {
now.scrollTop = scroll;
setTimeout(function () {
// When returning by history.back to an URL with a hash
// for the first time browser forces the scroll to the
// hash instead of the saved scroll position.
//
// This workaround prevents incorrect scroll position.
now.scrollTop = scroll;
}, 0);
}
now.classList.add(back ? 'hidden-left' : 'hidden-right');
@ -446,7 +497,12 @@ var IV = {
topBack.classList.remove('hidden');
}
IV.index = index;
IV.initMedia();
if (IV.cache[index].contentUpdated) {
IV.cache[index].contentUpdated = false;
IV.applyUpdatedContent(index);
} else {
IV.initMedia();
}
if (scroll === undefined) {
IV.jumpToHash(hash, true);
} else {

View File

@ -4,5 +4,6 @@
<file alias="page.js">../../iv_html/page.js</file>
<file alias="highlight.css">../../iv_html/highlight.9.12.0.css</file>
<file alias="highlight.js">../../iv_html/highlight.9.12.0.js</file>
<file alias="morphdom.js">../../iv_html/morphdom-umd.min.2.7.2.js</file>
</qresource>
</RCC>

View File

@ -281,6 +281,7 @@ bool WebPageData::applyChanges(
&& document == newDocument
&& collage.items == newCollage.items
&& (!iv == !newIv)
&& (!iv || iv->partial() == newIv->partial())
&& duration == newDuration
&& author == resultAuthor
&& hasLargeMedia == (newHasLargeMedia ? 1 : 0)

View File

@ -161,9 +161,7 @@ namespace {
<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();">
@ -193,6 +191,11 @@ namespace {
)"_q;
}
[[nodiscard]] QByteArray ReadResource(const QString &name) {
auto file = QFile(u":/iv/"_q + name);
return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
}
} // namespace
Controller::Controller()
@ -225,11 +228,28 @@ void Controller::show(
base::flat_map<QByteArray, rpl::producer<bool>> inChannelValues) {
page.script = fillInChannelValuesScript(std::move(inChannelValues));
_titleText.setText(st::ivTitle.style, page.title);
_title->update();
InvokeQueued(_container, [=, page = std::move(page)]() mutable {
showInWindow(dataPath, std::move(page));
});
}
void Controller::update(Prepared page) {
const auto url = page.url;
auto i = _indices.find(url);
if (i == end(_indices)) {
return;
}
const auto index = i->second;
_pages[index] = std::move(page);
if (_ready) {
_webview->eval(reloadScript(index));
} else if (!index) {
_reloadInitialWhenReady = true;
}
}
QByteArray Controller::fillInChannelValuesScript(
base::flat_map<QByteArray, rpl::producer<bool>> inChannelValues) {
auto result = QByteArray();
@ -426,6 +446,9 @@ void Controller::createWebview(const QString &dataPath) {
std::exchange(_navigateToIndexWhenReady, -1),
base::take(_navigateToHashWhenReady));
}
if (base::take(_reloadInitialWhenReady)) {
script += reloadScript(0);
}
if (!script.isEmpty()) {
_webview->eval(script);
}
@ -501,10 +524,13 @@ void Controller::createWebview(const QString &dataPath) {
const auto qstring = QString::fromUtf8(id.data(), id.size());
const auto pattern = u"^[a-zA-Z\\.\\-_0-9]+$"_q;
if (QRegularExpression(pattern).match(qstring).hasMatch()) {
auto file = QFile(u":/iv/"_q + qstring);
if (file.open(QIODevice::ReadOnly)) {
const auto bytes = ReadResource(qstring);
if (!bytes.isEmpty()) {
const auto mime = css ? "text/css" : "text/javascript";
return finishWith(file.readAll(), mime);
const auto full = (qstring == u"page.js"_q)
? (ReadResource("morphdom.js") + bytes)
: bytes;
return finishWith(full, mime);
}
}
return Webview::DataResult::Failed;
@ -560,6 +586,12 @@ QByteArray Controller::navigateScript(int index, const QString &hash) {
+ "');";
}
QByteArray Controller::reloadScript(int index) {
return "IV.reloadPage("
+ QByteArray::number(index)
+ ");";
}
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) {

View File

@ -55,6 +55,8 @@ public:
const QString &dataPath,
Prepared page,
base::flat_map<QByteArray, rpl::producer<bool>> inChannelValues);
void update(Prepared page);
[[nodiscard]] bool active() const;
void showJoinedTooltip();
void minimize();
@ -73,6 +75,7 @@ private:
void createWindow();
void createWebview(const QString &dataPath);
[[nodiscard]] QByteArray navigateScript(int index, const QString &hash);
[[nodiscard]] QByteArray reloadScript(int index);
void updateTitleGeometry();
void paintTitle(Painter &p, QRect clip);
@ -104,6 +107,7 @@ private:
base::flat_map<QByteArray, bool> _inChannelChanged;
base::flat_set<QByteArray> _inChannelSubscribed;
SingleQueuedInvokation _updateStyles;
bool _reloadInitialWhenReady = false;
bool _subscribedToColors = false;
bool _ready = false;

View File

@ -55,6 +55,10 @@ QString Data::id() const {
return qs(_source->page.data().vurl());
}
bool Data::partial() const {
return _source->page.data().is_part();
}
Data::~Data() = default;
void Data::prepare(const Options &options, Fn<void(Prepared)> done) const {

View File

@ -44,6 +44,7 @@ public:
~Data();
[[nodiscard]] QString id() const;
[[nodiscard]] bool partial() const;
void prepare(const Options &options, Fn<void(Prepared)> done) const;

View File

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/iv_instance.h"
#include "base/call_delayed.h"
#include "apiwrap.h"
#include "core/application.h"
#include "core/file_utilities.h"
@ -82,6 +83,7 @@ public:
[[nodiscard]] bool active() const;
void moveTo(not_null<Data*> data, QString hash);
void update(not_null<Data*> data);
void showJoinedTooltip();
void minimize();
@ -149,6 +151,7 @@ private:
void sendEmbed(QByteArray hash, Webview::DataRequest request);
void fillChannelJoinedValues(const Prepared &result);
void fillEmbeds(base::flat_map<QByteArray, QByteArray> added);
void subscribeToDocuments();
[[nodiscard]] QByteArray readFile(
const std::shared_ptr<::Data::DocumentMedia> &media);
@ -206,8 +209,8 @@ void Shown::prepare(not_null<Data*> data, const QString &hash) {
return;
}
_preparing = false;
_embeds = std::move(result.embeds);
fillChannelJoinedValues(result);
fillEmbeds(std::move(result.embeds));
if (!base.isEmpty()) {
_localBase = base;
showLocal(std::move(result));
@ -234,6 +237,16 @@ void Shown::fillChannelJoinedValues(const Prepared &result) {
}
}
void Shown::fillEmbeds(base::flat_map<QByteArray, QByteArray> added) {
if (_embeds.empty()) {
_embeds = std::move(added);
} else {
for (auto &[k, v] : added) {
_embeds[k] = std::move(v);
}
}
}
void Shown::showLocal(Prepared result) {
showProgress(0);
@ -783,6 +796,23 @@ void Shown::moveTo(not_null<Data*> data, QString hash) {
}
}
void Shown::update(not_null<Data*> data) {
const auto weak = base::make_weak(this);
const auto id = data->id();
const auto base = /*local ? LookupLocalPath(show) : */QString();
data->prepare({ .saveToFolder = base }, [=](Prepared result) {
crl::on_main(weak, [=, result = std::move(result)]() mutable {
result.url = id;
fillChannelJoinedValues(result);
fillEmbeds(std::move(result.embeds));
if (_controller) {
_controller->update(std::move(result));
}
});
});
}
void Shown::showJoinedTooltip() {
if (_controller) {
_controller->showJoinedTooltip();
@ -804,6 +834,13 @@ void Instance::show(
not_null<Data*> data,
QString hash) {
const auto session = &show->session();
const auto guard = gsl::finally([&] {
if (data->partial()) {
base::call_delayed(10000, [=] {
requestFull(session, data->id());
});
}
});
if (_shown && _shownSession == session) {
_shown->moveTo(data, hash);
return;
@ -891,7 +928,9 @@ void Instance::show(
) | rpl::start_with_next([=](const ::Data::PeerUpdate &update) {
if (const auto channel = update.peer->asChannel()) {
if (channel->amIn()) {
if (_joining.remove(not_null(channel))) {
const auto i = _joining.find(session);
const auto value = not_null{ channel };
if (i != end(_joining) && i->second.remove(value)) {
_shown->showJoinedTooltip();
}
}
@ -902,13 +941,8 @@ 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;
}
}
_joining.remove(session);
_fullRequested.remove(session);
if (_shownSession == session) {
_shownSession = nullptr;
}
@ -919,6 +953,27 @@ void Instance::show(
}
}
void Instance::requestFull(
not_null<Main::Session*> session,
const QString &id) {
if (!_tracking.contains(session)
|| !_fullRequested[session].emplace(id).second) {
return;
}
session->api().request(MTPmessages_GetWebPage(
MTP_string(id),
MTP_int(0)
)).done([=](const MTPmessages_WebPage &result) {
session->data().processUsers(result.data().vusers());
session->data().processChats(result.data().vchats());
const auto page = session->data().processWebpage(
result.data().vwebpage());
if (page && page->iv && _shown && _shownSession == session) {
_shown->update(page->iv.get());
}
}).send();
}
void Instance::processOpenChannel(const QString &context) {
if (!_shownSession) {
return;
@ -949,7 +1004,7 @@ void Instance::processJoinChannel(const QString &context) {
return;
} else if (const auto channelId = ChannelId(context.toLongLong())) {
const auto channel = _shownSession->data().channel(channelId);
_joining.emplace(channel);
_joining[_shownSession].emplace(channel);
if (channel->isLoaded()) {
_shownSession->api().joinChannel(channel);
} else if (!channel->username().isEmpty()) {

View File

@ -40,11 +40,17 @@ public:
private:
void processOpenChannel(const QString &context);
void processJoinChannel(const QString &context);
void requestFull(not_null<Main::Session*> session, const QString &id);
std::unique_ptr<Shown> _shown;
Main::Session *_shownSession = nullptr;
base::flat_set<not_null<Main::Session*>> _tracking;
base::flat_set<not_null<ChannelData*>> _joining;
base::flat_map<
not_null<Main::Session*>,
base::flat_set<not_null<ChannelData*>>> _joining;
base::flat_map<
not_null<Main::Session*>,
base::flat_set<QString>> _fullRequested;
rpl::lifetime _lifetime;