Add some javascript handlers to HTML export.

This commit is contained in:
John Preston 2018-07-24 21:28:04 +03:00
parent a99ae76ad4
commit aaa1245430
7 changed files with 329 additions and 47 deletions

View File

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

View File

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

View File

@ -39,6 +39,7 @@
<file alias="images/section_sessions@2x.png">../export_html/images/section_sessions@2x.png</file>
<file alias="images/section_web.png">../export_html/images/section_web.png</file>
<file alias="images/section_web@2x.png">../export_html/images/section_web@2x.png</file>
<file alias="js/script.js">../export_html/js/script.js</file>
</qresource>
<qresource prefix="/gui">
<file alias="fonts/OpenSans-Regular.ttf">../fonts/OpenSans-Regular.ttf</file>

View File

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

View File

@ -253,12 +253,14 @@ QByteArray FormatText(
+ internalLinksDomain.toUtf8()
+ text.mid(1)
+ "\">" + text + "</a>";
case Type::Hashtag: return "<a href=\"#hash-"
+ text.mid(1)
+ "\">" + text + "</a>";
case Type::BotCommand: return "<a href=\"#command-"
+ text.mid(1)
+ "\">" + text + "</a>";
case Type::Hashtag: return "<a href=\"\" "
"onclick=\"return ShowHashtag("
+ SerializeString('"' + text.mid(1) + '"')
+ ")\">" + text + "</a>";
case Type::BotCommand: return "<a href=\"\" "
"onclick=\"return ShowBotCommand("
+ SerializeString('"' + text.mid(1) + '"')
+ ")\">" + text + "</a>";
case Type::Url: return "<a href=\""
+ text
+ "\">" + text + "</a>";
@ -272,15 +274,15 @@ QByteArray FormatText(
case Type::TextUrl: return "<a href=\""
+ SerializeString(part.additional)
+ "\">" + text + "</a>";
case Type::MentionName: return "<a href=\"#mention-"
+ part.additional
+ "\">" + text + "</a>";
case Type::MentionName: return "<a href=\"\" "
"onclick=\"return ShowMentionName()\">" + text + "</a>";
case Type::Phone: return "<a href=\"tel:"
+ text
+ "\">" + text + "</a>";
case Type::Cashtag: return "<a href=\"#cash-"
+ text.mid(1)
+ "\">" + text + "</a>";
case Type::Cashtag: return "<a href=\"\" "
"onclick=\"return ShowCashtag("
+ SerializeString('"' + text.mid(1) + '"')
+ ")\">" + text + "</a>";
}
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<QByteArray(int messageId, QByteArray text)> 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<QByteArray(int messageId, QByteArray text)> wrapMessageLink
) -> std::pair<MessageInfo, QByteArray> {
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 "<a href=\"#message"
+ NumberToString(message.replyToMsgId)
+ "\">"
+ text + "</a>";
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<MessageInfo>();
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<MessageInfo>(*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 "<a href=\"#go_to_message"
+ Data::NumberToString(messageId)
+ "\" onclick=\"return GoToMessage("
+ Data::NumberToString(messageId)
+ ")\">"
+ text + "</a>";
} else {
const auto index = it - begin(_lastMessageIdsPerFile);
return "<a href=\"" + messagesFile(index).toUtf8()
+ "#go_to_message"
+ Data::NumberToString(messageId)
+ "\">"
+ text + "</a>";
}
}
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());

View File

@ -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<Wrap> _chats;
std::unique_ptr<Wrap> _chat;
std::vector<int> _lastMessageIdsPerFile;
bool _chatFileEmpty = false;
};

View File

@ -110,6 +110,7 @@
'<!@(<(list_sources_command) <(qt_moc_list_sources_arg))',
'telegram_sources.txt',
'<(res_loc)/export_html/css/style.css',
'<(res_loc)/export_html/js/script.js',
'<(res_loc)/export_html/images/back.png',
'<(res_loc)/export_html/images/back@2x.png',
],