From aaa1245430fc0fa47c4898eade996f0e586cac1d Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 24 Jul 2018 21:28:04 +0300 Subject: [PATCH] Add some javascript handlers to HTML export. --- Telegram/Resources/export_html/css/style.css | 30 ++- Telegram/Resources/export_html/js/script.js | 189 ++++++++++++++++++ Telegram/Resources/qrc/telegram.qrc | 1 + .../SourceFiles/export/export_api_wrap.cpp | 10 + .../export/output/export_output_html.cpp | 140 ++++++++----- .../export/output/export_output_html.h | 5 + Telegram/gyp/Telegram.gyp | 1 + 7 files changed, 329 insertions(+), 47 deletions(-) create mode 100644 Telegram/Resources/export_html/js/script.js diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css index d873e35f8..0e0aa9d9e 100644 --- a/Telegram/Resources/export_html/css/style.css +++ b/Telegram/Resources/export_html/css/style.css @@ -245,6 +245,14 @@ a.block_link:hover { .history { padding: 16px 0; } +.message { + margin: 0 -10px; + transition: background-color 2.0s ease; +} +div.selected { + background-color: rgba(242,246,250,255); + transition: background-color 0.5s ease; +} .service { padding: 10px 24px; } @@ -264,7 +272,7 @@ a.block_link:hover { font-size: 16px; } .default { - padding: 10px 0 10px; + padding: 10px; } .default.joined { padding-top: 0; @@ -379,6 +387,26 @@ a.block_link:hover { font-size: 16px; } +.toast_container { + position: fixed; + left: 50%; + top: 50%; + opacity: 0; + transition: opacity 3.0s ease; +} +.toast_body { + margin: 0 -50%; + float: left; + border-radius: 15px; + padding: 10px 20px; + background: rgba(0, 0, 0, 0.7); + color: #ffffff; +} +div.toast_shown { + opacity: 1; + transition: opacity 0.4s ease; +} + .section.calls { background-image: url(../images/section_calls.png); } diff --git a/Telegram/Resources/export_html/js/script.js b/Telegram/Resources/export_html/js/script.js new file mode 100644 index 000000000..f206440a0 --- /dev/null +++ b/Telegram/Resources/export_html/js/script.js @@ -0,0 +1,189 @@ +"use strict"; + +window.AllowBackFromHistory = false; +function CheckLocation() { + var start = "#go_to_message"; + var hash = location.hash; + if (hash.substr(0, start.length) == start) { + var messageId = parseInt(hash.substr(start.length)); + if (messageId) { + GoToMessage(messageId); + } + } else if (hash == "#allow_back") { + window.AllowBackFromHistory = true; + } +} + +function ShowToast(text) { + var container = document.createElement("div"); + container.className = "toast_container"; + var inner = container.appendChild(document.createElement("div")); + inner.className = "toast_body"; + inner.appendChild(document.createTextNode(text)); + var appended = document.body.appendChild(container); + setTimeout(function () { + AddClass(appended, "toast_shown"); + setTimeout(function () { + RemoveClass(appended, "toast_shown"); + setTimeout(function () { + document.body.removeChild(appended); + }, 3000); + }, 3000); + }, 0); +} + +function ShowHashtag(tag) { + ShowToast("This is a hashtag '#" + tag + "' link."); + return false; +} + +function ShowCashtag(tag) { + ShowToast("This is a cashtag '$" + tag + "' link."); + return false; +} + +function ShowBotCommand(command) { + ShowToast("This is a bot command '/" + command + "' link."); + return false; +} + +function ShowMentionName() { + ShowToast("This is a link to a user mentioned by name."); + return false; +} + +function AddClass(element, name) { + var current = element.className; + var expression = new RegExp('(^|\\s)' + name + '(\\s|$)', 'g'); + if (expression.test(current)) { + return; + } + element.className = current + ' ' + name; +} + +function RemoveClass(element, name) { + var current = element.className; + var expression = new RegExp('(^|\\s)' + name + '(\\s|$)', ''); + var match = expression.exec(current); + while ((match = expression.exec(current)) != null) { + if (match[1].length > 0 && match[2].length > 0) { + current = current.substr(0, match.index + match[1].length) + + current.substr(match.index + match[0].length); + } else { + current = current.substr(0, match.index) + + current.substr(match.index + match[0].length); + } + } + element.className = current; +} + +function EaseOutQuad(t) { + return t * t; +} + +function EaseInOutQuad(t) { + return (t < 0.5) ? (2 * t * t) : ((4 - 2 * t) * t - 1); +} + +function ScrollHeight() { + if ("innerHeight" in window) { + return window.innerHeight; + } else if (document.documentElement) { + return document.documentElement.clientHeight; + } + return document.body.clientHeight; +} + +function ScrollTo(top, callback) { + var html = document.documentElement; + var current = html.scrollTop; + var delta = top - current; + var finish = function () { + html.scrollTop = top; + if (callback) { + callback(); + } + }; + if (!window.performance.now || delta == 0) { + finish(); + return; + } + var transition = EaseOutQuad; + var max = 300; + if (delta < -max) { + current = top + max; + delta = -max; + } else if (delta > max) { + current = top - max; + delta = max; + } else { + transition = EaseInOutQuad; + } + var duration = 150; + var interval = 7; + var time = window.performance.now(); + var animate = function () { + var now = window.performance.now(); + if (now >= time + duration) { + finish(); + return; + } + var dt = (now - time) / duration; + html.scrollTop = Math.round(current + delta * transition(dt)); + setTimeout(animate, interval); + }; + setTimeout(animate, interval); +} + +function ScrollToElement(element, callback) { + var header = document.getElementsByClassName("page_header")[0]; + var headerHeight = header.offsetHeight; + var html = document.documentElement; + var scrollHeight = ScrollHeight(); + var available = scrollHeight - headerHeight; + var padding = 10; + var top = element.offsetTop; + var height = element.offsetHeight; + var desired = top + - Math.max((available - height) / 2, padding) + - headerHeight; + var scrollTopMax = html.offsetHeight - scrollHeight; + ScrollTo(Math.min(desired, scrollTopMax), callback); +} + +function GoToMessage(messageId) { + var element = document.getElementById("message" + messageId); + if (element) { + var hash = "#go_to_message" + messageId; + if (location.hash != hash) { + location.hash = hash; + } + ScrollToElement(element, function () { + AddClass(element, "selected"); + setTimeout(function () { + RemoveClass(element, "selected"); + }, 1000); + }); + } else { + ShowToast("This message was not exported. Maybe it was deleted."); + } + return false; +} + +function GoBack(anchor) { + if (!window.AllowBackFromHistory) { + return true; + } + history.back(); + if (!anchor || !anchor.getAttribute) { + return true; + } + var destination = anchor.getAttribute("href"); + if (!destination) { + return true; + } + setTimeout(function () { + location.href = destination; + }, 100); + return false; +} diff --git a/Telegram/Resources/qrc/telegram.qrc b/Telegram/Resources/qrc/telegram.qrc index e96b25780..91cbeee5c 100644 --- a/Telegram/Resources/qrc/telegram.qrc +++ b/Telegram/Resources/qrc/telegram.qrc @@ -39,6 +39,7 @@ ../export_html/images/section_sessions@2x.png ../export_html/images/section_web.png ../export_html/images/section_web@2x.png + ../export_html/js/script.js ../fonts/OpenSans-Regular.ttf diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index aba06f381..3cceb5064 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -972,6 +972,13 @@ void ApiWrap::cancelExportFast() { } void ApiWrap::requestSinglePeerDialog() { + const auto isChannelType = [](Data::DialogInfo::Type type) { + using Type = Data::DialogInfo::Type; + return (type == Type::PrivateSupergroup) + || (type == Type::PublicSupergroup) + || (type == Type::PrivateChannel) + || (type == Type::PublicChannel); + }; auto doneSinglePeer = [=](const auto &result) { auto info = Data::ParseDialogsInfo(_settings->singlePeer, result); @@ -980,6 +987,9 @@ void ApiWrap::requestSinglePeerDialog() { const auto last = _dialogsProcess->splitIndexPlusOne - 1; for (auto &info : _dialogsProcess->info.chats) { + if (isChannelType(info.type)) { + continue; + } for (auto i = last; i != 0; --i) { info.splits.push_back(i - 1); info.messagesCountPerSplit.push_back(0); diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 74b090410..de26882a2 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -253,12 +253,14 @@ QByteArray FormatText( + internalLinksDomain.toUtf8() + text.mid(1) + "\">" + text + ""; - case Type::Hashtag: return "" + text + ""; - case Type::BotCommand: return "" + text + ""; + case Type::Hashtag: return "" + text + ""; + case Type::BotCommand: return "" + text + ""; case Type::Url: return "" + text + ""; @@ -272,15 +274,15 @@ QByteArray FormatText( case Type::TextUrl: return "" + text + ""; - case Type::MentionName: return "" + text + ""; + case Type::MentionName: return "" + text + ""; case Type::Phone: return "" + text + ""; - case Type::Cashtag: return "" + text + ""; + case Type::Cashtag: return "" + text + ""; } Unexpected("Type in text entities serialization."); }) | ranges::to_vector); @@ -506,6 +508,7 @@ struct HtmlWriter::MessageInfo { Service, Default, }; + int32 id = 0; Type type = Type::Service; int32 fromId = 0; TimeId date = 0; @@ -566,7 +569,8 @@ public: const Data::DialogInfo &dialog, const QString &basePath, const PeersMap &peers, - const QString &internalLinksDomain); + const QString &internalLinksDomain, + Fn wrapMessageLink); [[nodiscard]] Result writeBlock(const QByteArray &block); @@ -794,7 +798,7 @@ QByteArray HtmlWriter::Wrap::pushGenericListEntry( ? pushDiv("entry clearfix") : pushTag("a", { { "class", "entry block_link clearfix" }, - { "href", relativePath(link).toUtf8() }, + { "href", relativePath(link).toUtf8() + "#allow_back" }, }); result.append(pushDiv("pull_left userpic_wrap")); result.append(pushUserpic(userpic)); @@ -850,7 +854,8 @@ QByteArray HtmlWriter::Wrap::pushHeader( ? pushDiv("content") : pushTag("a", { { "class", "content block_link" }, - { "href", relativePath(path).toUtf8() } + { "href", relativePath(path).toUtf8() }, + { "onclick", "return GoBack(this)"}, })); result.append(pushDiv("text bold")); result.append(SerializeString(header)); @@ -867,7 +872,7 @@ QByteArray HtmlWriter::Wrap::pushSection( const QString &link) { auto result = pushTag("a", { { "class", "section block_link " + type }, - { "href", link.toUtf8() }, + { "href", link.toUtf8() + "#allow_back" }, }); result.append(pushDiv("counter details")); result.append(Data::NumberToString(count)); @@ -924,16 +929,18 @@ QByteArray HtmlWriter::Wrap::pushServiceMessage( } auto HtmlWriter::Wrap::pushMessage( - const Data::Message &message, - const MessageInfo *previous, - const Data::DialogInfo &dialog, - const QString &basePath, - const PeersMap &peers, - const QString &internalLinksDomain + const Data::Message &message, + const MessageInfo *previous, + const Data::DialogInfo &dialog, + const QString &basePath, + const PeersMap &peers, + const QString &internalLinksDomain, + Fn wrapMessageLink ) -> std::pair { using namespace Data; auto info = MessageInfo(); + info.id = message.id; info.fromId = message.fromId; info.date = message.date; info.forwardedFromId = message.forwardedFromId; @@ -948,10 +955,7 @@ auto HtmlWriter::Wrap::pushMessage( } const auto wrapReplyToLink = [&](const QByteArray &text) { - return "" - + text + ""; + return wrapMessageLink(message.replyToMsgId, text); }; const auto serviceFrom = peers.wrapUserName(message.fromId); @@ -1706,8 +1710,15 @@ QByteArray HtmlWriter::Wrap::composeStart() { { "rel", "stylesheet" }, { "empty", "" } })); + result.append(_context.pushTag("script", { + { "src", _base + "js/script.js" }, + { "type", "text/javascript" }, + })); + result.append(_context.popTag()); result.append(popTag()); - result.append(pushTag("body")); + result.append(pushTag("body", { + { "onload", "CheckLocation();" } + })); result.append(pushDiv("page_wrap")); return result; } @@ -1758,6 +1769,7 @@ Result HtmlWriter::start( "images/section_photos.png", "images/section_sessions.png", "images/section_web.png", + "js/script.js", }; for (const auto path : files) { const auto name = QString(path); @@ -2239,6 +2251,7 @@ Result HtmlWriter::writeDialogStart(const Data::DialogInfo &data) { _messagesCount = 0; _dateMessageId = 0; _lastMessageInfo = nullptr; + _lastMessageIdsPerFile.clear(); _dialog = data; return Result::Success(); } @@ -2247,11 +2260,32 @@ Result HtmlWriter::writeDialogSlice(const Data::MessagesSlice &data) { Expects(_chat != nullptr); Expects(!data.list.empty()); + const auto messageLinkWrapper = [&](int messageId, QByteArray text) { + return wrapMessageLink(messageId, text); + }; auto oldIndex = (_messagesCount / kMessagesInFile); auto previous = _lastMessageInfo.get(); auto saved = base::optional(); auto block = QByteArray(); for (const auto &message : data.list) { + const auto newIndex = (_messagesCount / kMessagesInFile); + if (oldIndex != newIndex) { + if (const auto result = _chat->writeBlock(block); !result) { + return result; + } else if (const auto next = switchToNextChatFile(newIndex)) { + Assert(saved.has_value() || _lastMessageInfo != nullptr); + _lastMessageIdsPerFile.push_back(saved + ? saved->id + : _lastMessageInfo->id); + block = QByteArray(); + _lastMessageInfo = nullptr; + previous = nullptr; + saved = base::none; + oldIndex = newIndex; + } else { + return next; + } + } if (_chatFileEmpty) { if (const auto result = writeDialogOpening(oldIndex); !result) { return result; @@ -2272,27 +2306,13 @@ Result HtmlWriter::writeDialogSlice(const Data::MessagesSlice &data) { _dialog, _settings.path, data.peers, - _environment.internalLinksDomain); + _environment.internalLinksDomain, + messageLinkWrapper); block.append(content); ++_messagesCount; - const auto newIndex = (_messagesCount / kMessagesInFile); - if (oldIndex != newIndex) { - if (const auto result = _chat->writeBlock(block); !result) { - return result; - } else if (const auto next = switchToNextChatFile(newIndex)) { - block = QByteArray(); - _lastMessageInfo = nullptr; - previous = nullptr; - saved = base::none; - oldIndex = newIndex; - } else { - return next; - } - } else { - saved = info; - previous = &*saved; - } + saved = info; + previous = &*saved; } if (saved) { _lastMessageInfo = std::make_unique(*saved); @@ -2474,7 +2494,9 @@ void HtmlWriter::pushSection( Result HtmlWriter::writeSections() { Expects(_summary != nullptr); - if (!_haveSections) { + if (_savedSections.empty()) { + return Result::Success(); + } else if (!_haveSections) { auto block = _summary->pushDiv( _summaryNeedDivider ? "sections with_divider" : "sections"); if (const auto result = _summary->writeBlock(block); !result) { @@ -2498,6 +2520,29 @@ Result HtmlWriter::writeSections() { return _summary->writeBlock(block); } +QByteArray HtmlWriter::wrapMessageLink(int messageId, QByteArray text) { + const auto finishedCount = _lastMessageIdsPerFile.size(); + const auto it = ranges::find_if(_lastMessageIdsPerFile, [&](int maxMessageId) { + return messageId <= maxMessageId; + }); + if (it == end(_lastMessageIdsPerFile)) { + return "" + + text + ""; + } else { + const auto index = it - begin(_lastMessageIdsPerFile); + return "" + + text + ""; + + } +} + Result HtmlWriter::switchToNextChatFile(int index) { Expects(_chat != nullptr); @@ -2525,6 +2570,9 @@ Result HtmlWriter::finish() { return Result::Success(); } + if (const auto result = writeSections(); !result) { + return result; + } auto block = QByteArray(); if (_haveSections) { block.append(_summary->popTag()); diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h index 75de9c7e2..a2235d130 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.h +++ b/Telegram/SourceFiles/export/output/export_output_html.h @@ -128,6 +128,10 @@ private: [[nodiscard]] QString userpicsFilePath() const; + [[nodiscard]] QByteArray wrapMessageLink( + int messageId, + QByteArray text); + Settings _settings; Environment _environment; Stats *_stats = nullptr; @@ -154,6 +158,7 @@ private: int _dateMessageId = 0; std::unique_ptr _chats; std::unique_ptr _chat; + std::vector _lastMessageIdsPerFile; bool _chatFileEmpty = false; }; diff --git a/Telegram/gyp/Telegram.gyp b/Telegram/gyp/Telegram.gyp index f959b65f0..43cce710f 100644 --- a/Telegram/gyp/Telegram.gyp +++ b/Telegram/gyp/Telegram.gyp @@ -110,6 +110,7 @@ '