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