diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css
index 8048f5c56..63c444d2a 100644
--- a/Telegram/Resources/export_html/css/style.css
+++ b/Telegram/Resources/export_html/css/style.css
@@ -245,3 +245,18 @@ a.block_link:hover {
.history {
padding: 16px 0;
}
+.service {
+ padding: 10px 24px;
+}
+.service .content {
+ text-align: center;
+}
+.service .userpic_wrap {
+ padding-top: 10px;
+}
+.service .userpic {
+ margin: 0 auto;
+}
+.service .userpic .initials {
+ font-size: 24px;
+}
diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp
index 40b3f222c..0acaf11c2 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_html.cpp
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/utils.h"
#include
+#include
namespace Export {
namespace Output {
@@ -20,6 +21,7 @@ namespace {
constexpr auto kMessagesInFile = 1000;
constexpr auto kPersonalUserpicSize = 90;
constexpr auto kEntryUserpicSize = 48;
+constexpr auto kServiceMessagePhotoSize = 60;
constexpr auto kSavedMessagesColorIndex = 3;
const auto kLineBreak = QByteArrayLiteral("
");
@@ -193,416 +195,46 @@ QByteArray FormatFilePath(const Data::File &file) {
return file.relativePath.toUtf8();
}
+bool DisplayDate(TimeId date, TimeId previousDate) {
+ if (!previousDate) {
+ return true;
+ }
+ return QDateTime::fromTime_t(date).date()
+ != QDateTime::fromTime_t(previousDate).date();
+}
+
+QByteArray FormatDateText(TimeId date) {
+ const auto parsed = QDateTime::fromTime_t(date).date();
+ const auto month = [](int index) {
+ switch (index) {
+ case 1: return "January";
+ case 2: return "February";
+ case 3: return "March";
+ case 4: return "April";
+ case 5: return "May";
+ case 6: return "June";
+ case 7: return "July";
+ case 8: return "August";
+ case 9: return "September";
+ case 10: return "October";
+ case 11: return "November";
+ case 12: return "December";
+ }
+ return "Unknown";
+ };
+ return Data::NumberToString(parsed.day())
+ + ' '
+ + month(parsed.month())
+ + ' '
+ + Data::NumberToString(parsed.year());
+}
+
QByteArray SerializeLink(
const Data::Utf8String &text,
const QString &path) {
return "" + text + "";
}
-QByteArray SerializeMessage(
- Fn relativePath,
- const Data::Message &message,
- const std::map &peers,
- const QString &internalLinksDomain) {
- using namespace Data;
-
- if (message.media.content.is()) {
- return SerializeString("Error! This message is not supported "
- "by this version of Telegram Desktop. "
- "Please update the application.");
- }
-
- const auto peer = [&](PeerId peerId) -> const Peer& {
- if (const auto i = peers.find(peerId); i != end(peers)) {
- return i->second;
- }
- static auto empty = Peer{ User() };
- return empty;
- };
- const auto user = [&](int32 userId) -> const User& {
- if (const auto result = peer(UserPeerId(userId)).user()) {
- return *result;
- }
- static auto empty = User();
- return empty;
- };
- const auto chat = [&](int32 chatId) -> const Chat& {
- if (const auto result = peer(ChatPeerId(chatId)).chat()) {
- return *result;
- }
- static auto empty = Chat();
- return empty;
- };
-
- auto values = std::vector>{
- { "ID", SerializeString(NumberToString(message.id)) },
- { "Date", SerializeString(FormatDateTime(message.date)) },
- { "Edited", SerializeString(FormatDateTime(message.edited)) },
- };
- const auto pushBare = [&](
- const QByteArray &key,
- const QByteArray &value) {
- values.emplace_back(key, value);
- };
- const auto push = [&](const QByteArray &key, const QByteArray &value) {
- if (!value.isEmpty()) {
- pushBare(key, SerializeString(value));
- }
- };
- const auto wrapPeerName = [&](PeerId peerId) {
- const auto result = peer(peerId).name();
- return result.isEmpty() ? QByteArray("(deleted peer)") : result;
- };
- const auto wrapUserName = [&](int32 userId) {
- const auto result = user(userId).name();
- return result.isEmpty() ? QByteArray("(deleted user)") : result;
- };
- const auto pushFrom = [&](const QByteArray &label = "From") {
- if (message.fromId) {
- push(label, wrapUserName(message.fromId));
- }
- };
- const auto pushReplyToMsgId = [&](
- const QByteArray &label = "Reply to message") {
- if (message.replyToMsgId) {
- push(label, "ID-" + NumberToString(message.replyToMsgId));
- }
- };
- const auto pushUserNames = [&](
- const std::vector &data,
- const QByteArray &labelOne = "Member",
- const QByteArray &labelMany = "Members") {
- auto list = std::vector();
- for (const auto userId : data) {
- list.push_back(SerializeString(wrapUserName(userId)));
- }
- if (list.size() == 1) {
- pushBare(labelOne, list[0]);
- } else if (!list.empty()) {
- pushBare(labelMany, JoinList(", ", list));
- }
- };
- const auto pushActor = [&] {
- pushFrom("Actor");
- };
- const auto pushAction = [&](const QByteArray &action) {
- push("Action", action);
- };
- const auto pushTTL = [&](
- const QByteArray &label = "Self destruct period") {
- if (const auto ttl = message.media.ttl) {
- push(label, NumberToString(ttl) + " sec.");
- }
- };
-
- using SkipReason = Data::File::SkipReason;
- const auto formatPath = [&](
- const Data::File &file,
- const QByteArray &label,
- const QByteArray &name = QByteArray()) {
- Expects(!file.relativePath.isEmpty()
- || file.skipReason != SkipReason::None);
-
- const auto pre = name.isEmpty()
- ? QByteArray()
- : SerializeString(name + ' ');
- switch (file.skipReason) {
- case SkipReason::Unavailable:
- return pre + "(" + label + " unavailable, "
- "please try again later)";
- case SkipReason::FileSize:
- return pre + "(" + label + " exceeds maximum size. "
- "Change data exporting settings to download.)";
- case SkipReason::FileType:
- return pre + "(" + label + " not included. "
- "Change data exporting settings to download.)";
- case SkipReason::None: return SerializeLink(
- FormatFilePath(file),
- relativePath(file.relativePath));
- }
- Unexpected("Skip reason while writing file path.");
- };
- const auto pushPath = [&](
- const Data::File &file,
- const QByteArray &label,
- const QByteArray &name = QByteArray()) {
- pushBare(label, formatPath(file, label, name));
- };
- const auto pushPhoto = [&](const Image &image) {
- pushPath(image.file, "Photo");
- if (image.width && image.height) {
- push("Width", NumberToString(image.width));
- push("Height", NumberToString(image.height));
- }
- };
-
- message.action.content.match([&](const ActionChatCreate &data) {
- pushActor();
- pushAction("Create group");
- push("Title", data.title);
- pushUserNames(data.userIds);
- }, [&](const ActionChatEditTitle &data) {
- pushActor();
- pushAction("Edit group title");
- push("New title", data.title);
- }, [&](const ActionChatEditPhoto &data) {
- pushActor();
- pushAction("Edit group photo");
- pushPhoto(data.photo.image);
- }, [&](const ActionChatDeletePhoto &data) {
- pushActor();
- pushAction("Delete group photo");
- }, [&](const ActionChatAddUser &data) {
- pushActor();
- pushAction("Invite members");
- pushUserNames(data.userIds);
- }, [&](const ActionChatDeleteUser &data) {
- pushActor();
- pushAction("Remove members");
- push("Member", wrapUserName(data.userId));
- }, [&](const ActionChatJoinedByLink &data) {
- pushActor();
- pushAction("Join group by link");
- push("Inviter", wrapUserName(data.inviterId));
- }, [&](const ActionChannelCreate &data) {
- pushActor();
- pushAction("Create channel");
- push("Title", data.title);
- }, [&](const ActionChatMigrateTo &data) {
- pushActor();
- pushAction("Convert this group to supergroup");
- }, [&](const ActionChannelMigrateFrom &data) {
- pushActor();
- pushAction("Basic group converted to supergroup");
- push("Title", data.title);
- }, [&](const ActionPinMessage &data) {
- pushActor();
- pushAction("Pin message");
- pushReplyToMsgId("Message");
- }, [&](const ActionHistoryClear &data) {
- pushActor();
- pushAction("Clear history");
- }, [&](const ActionGameScore &data) {
- pushActor();
- pushAction("Score in a game");
- pushReplyToMsgId("Game message");
- push("Score", NumberToString(data.score));
- }, [&](const ActionPaymentSent &data) {
- pushAction("Send payment");
- push(
- "Amount",
- Data::FormatMoneyAmount(data.amount, data.currency));
- pushReplyToMsgId("Invoice message");
- }, [&](const ActionPhoneCall &data) {
- pushActor();
- pushAction("Phone call");
- if (data.duration) {
- push("Duration", NumberToString(data.duration) + " sec.");
- }
- using Reason = ActionPhoneCall::DiscardReason;
- push("Discard reason", [&] {
- switch (data.discardReason) {
- case Reason::Busy: return "Busy";
- case Reason::Disconnect: return "Disconnect";
- case Reason::Hangup: return "Hangup";
- case Reason::Missed: return "Missed";
- }
- return "";
- }());
- }, [&](const ActionScreenshotTaken &data) {
- pushActor();
- pushAction("Take screenshot");
- }, [&](const ActionCustomAction &data) {
- pushActor();
- push("Information", data.message);
- }, [&](const ActionBotAllowed &data) {
- pushAction("Allow sending messages");
- push("Reason", "Login on \"" + data.domain + "\"");
- }, [&](const ActionSecureValuesSent &data) {
- pushAction("Send Telegram Passport values");
- auto list = std::vector();
- for (const auto type : data.types) {
- list.push_back([&] {
- using Type = ActionSecureValuesSent::Type;
- switch (type) {
- case Type::PersonalDetails: return "Personal details";
- case Type::Passport: return "Passport";
- case Type::DriverLicense: return "Driver license";
- case Type::IdentityCard: return "Identity card";
- case Type::InternalPassport: return "Internal passport";
- case Type::Address: return "Address information";
- case Type::UtilityBill: return "Utility bill";
- case Type::BankStatement: return "Bank statement";
- case Type::RentalAgreement: return "Rental agreement";
- case Type::PassportRegistration:
- return "Passport registration";
- case Type::TemporaryRegistration:
- return "Temporary registration";
- case Type::Phone: return "Phone number";
- case Type::Email: return "Email";
- }
- return "";
- }());
- }
- if (list.size() == 1) {
- push("Value", list[0]);
- } else if (!list.empty()) {
- push("Values", JoinList(", ", list));
- }
- }, [](const base::none_type &) {});
-
- if (!message.action.content) {
- pushFrom();
- push("Author", message.signature);
- if (message.forwardedFromId) {
- push("Forwarded from", wrapPeerName(message.forwardedFromId));
- }
- if (message.savedFromChatId) {
- push("Saved from", wrapPeerName(message.savedFromChatId));
- }
- pushReplyToMsgId();
- if (message.viaBotId) {
- push("Via", user(message.viaBotId).username);
- }
- }
-
- message.media.content.match([&](const Photo &photo) {
- pushPhoto(photo.image);
- pushTTL();
- }, [&](const Document &data) {
- const auto pushMyPath = [&](const QByteArray &label) {
- return pushPath(data.file, label);
- };
- if (data.isSticker) {
- pushMyPath("Sticker");
- push("Emoji", data.stickerEmoji);
- } else if (data.isVideoMessage) {
- pushMyPath("Video message");
- } else if (data.isVoiceMessage) {
- pushMyPath("Voice message");
- } else if (data.isAnimated) {
- pushMyPath("Animation");
- } else if (data.isVideoFile) {
- pushMyPath("Video file");
- } else if (data.isAudioFile) {
- pushMyPath("Audio file");
- push("Performer", data.songPerformer);
- push("Title", data.songTitle);
- } else {
- pushMyPath("File");
- }
- if (!data.isSticker) {
- push("Mime type", data.mime);
- }
- if (data.duration) {
- push("Duration", NumberToString(data.duration) + " sec.");
- }
- if (data.width && data.height) {
- push("Width", NumberToString(data.width));
- push("Height", NumberToString(data.height));
- }
- pushTTL();
- }, [&](const SharedContact &data) {
- pushBare("Contact information", SerializeBlockquote({
- { "First name", data.info.firstName },
- { "Last name", data.info.lastName },
- { "Phone number", FormatPhoneNumber(data.info.phoneNumber) },
- { "vCard", (data.vcard.content.isEmpty()
- ? QByteArray()
- : formatPath(data.vcard, "vCard")) }
- }));
- }, [&](const GeoPoint &data) {
- pushBare("Location", data.valid ? SerializeBlockquote({
- { "Latitude", NumberToString(data.latitude) },
- { "Longitude", NumberToString(data.longitude) },
- }) : QByteArray("(empty value)"));
- pushTTL("Live location period");
- }, [&](const Venue &data) {
- push("Place name", data.title);
- push("Address", data.address);
- if (data.point.valid) {
- pushBare("Location", SerializeBlockquote({
- { "Latitude", NumberToString(data.point.latitude) },
- { "Longitude", NumberToString(data.point.longitude) },
- }));
- }
- }, [&](const Game &data) {
- push("Game", data.title);
- push("Description", data.description);
- if (data.botId != 0 && !data.shortName.isEmpty()) {
- const auto bot = user(data.botId);
- if (bot.isBot && !bot.username.isEmpty()) {
- push("Link", internalLinksDomain.toUtf8()
- + bot.username
- + "?game="
- + data.shortName);
- }
- }
- }, [&](const Invoice &data) {
- pushBare("Invoice", SerializeBlockquote({
- { "Title", data.title },
- { "Description", data.description },
- {
- "Amount",
- Data::FormatMoneyAmount(data.amount, data.currency)
- },
- { "Receipt message", (data.receiptMsgId
- ? "ID-" + NumberToString(data.receiptMsgId)
- : QByteArray()) }
- }));
- }, [](const UnsupportedMedia &data) {
- Unexpected("Unsupported message.");
- }, [](const base::none_type &) {});
-
- auto value = JoinList(QByteArray(), ranges::view::all(
- message.text
- ) | ranges::view::transform([&](const Data::TextPart &part) {
- const auto text = SerializeString(part.text);
- using Type = Data::TextPart::Type;
- switch (part.type) {
- case Type::Text: return text;
- case Type::Unknown: return text;
- case Type::Mention:
- return "" + text + "";
- case Type::Hashtag: return "" + text + "";
- case Type::BotCommand: return "" + text + "";
- case Type::Url: return "" + text + "";
- case Type::Email: return "" + text + "";
- case Type::Bold: return "" + text + "";
- case Type::Italic: return "" + text + "";
- case Type::Code: return "" + text + "
";
- case Type::Pre: return "" + text + "
";
- case Type::TextUrl: return "" + text + "";
- case Type::MentionName: return "" + text + "";
- case Type::Phone: return "" + text + "";
- case Type::Cashtag: return "" + text + "";
- }
- Unexpected("Type in text entities serialization.");
- }) | ranges::to_vector);
- pushBare("Text", value);
-
- return SerializeKeyValue(std::move(values));
-}
-
} // namespace
namespace details {
@@ -703,6 +335,18 @@ public:
[[nodiscard]] QByteArray pushAbout(
const QByteArray &text,
bool withDivider = false);
+ [[nodiscard]] QByteArray pushServiceMessage(
+ int messageId,
+ const Data::DialogInfo &dialog,
+ const QString &basePath,
+ const QByteArray &text,
+ const Data::Photo *photo = nullptr);
+ [[nodiscard]] QByteArray pushMessage(
+ const Data::Message &message,
+ const Data::DialogInfo &dialog,
+ const QString &basePath,
+ const std::map &peers,
+ const QString &internalLinksDomain);
[[nodiscard]] Result writeBlock(const QByteArray &block);
@@ -744,6 +388,19 @@ QByteArray ComposeName(const UserpicData &data, const QByteArray &empty) {
: (data.firstName + ' ' + data.lastName));
}
+QString WriteUserpicThumb(
+ const QString &basePath,
+ const QString &largePath,
+ const UserpicData &userpic,
+ const QString &postfix = "_thumb") {
+ return Data::WriteImageThumb(
+ basePath,
+ largePath,
+ userpic.pixelSize * 2,
+ userpic.pixelSize * 2,
+ postfix);
+}
+
HtmlWriter::Wrap::Wrap(
const QString &path,
const QString &base,
@@ -973,6 +630,451 @@ QByteArray HtmlWriter::Wrap::pushAbout(
return result;
}
+QByteArray HtmlWriter::Wrap::pushServiceMessage(
+ int messageId,
+ const Data::DialogInfo &dialog,
+ const QString &basePath,
+ const QByteArray &serialized,
+ const Data::Photo *photo) {
+ auto result = pushTag("div", {
+ { "class", "message service" },
+ { "id", "message" + Data::NumberToString(messageId) }
+ });
+ result.append(pushDiv("content details"));
+ result.append(serialized);
+ result.append(popTag());
+ if (photo) {
+ auto userpic = UserpicData();
+ userpic.colorIndex = Data::PeerColorIndex(
+ Data::BarePeerId(dialog.peerId));
+ userpic.firstName = dialog.name;
+ userpic.lastName = dialog.lastName;
+ userpic.pixelSize = kServiceMessagePhotoSize;
+ userpic.largeLink = photo->image.file.relativePath;
+ userpic.imageLink = WriteUserpicThumb(
+ basePath,
+ userpic.largeLink,
+ userpic);
+ result.append(pushDiv("userpic_wrap"));
+ result.append(pushUserpic(userpic));
+ result.append(popTag());
+ }
+ result.append(popTag());
+ return result;
+}
+
+QByteArray HtmlWriter::Wrap::pushMessage(
+ const Data::Message &message,
+ const Data::DialogInfo &dialog,
+ const QString &basePath,
+ const std::map &peers,
+ const QString &internalLinksDomain) {
+ using namespace Data;
+
+ if (message.media.content.is()) {
+ return pushServiceMessage(
+ message.id,
+ dialog,
+ basePath,
+ "This message is not supported by this version "
+ "of Telegram Desktop. Please update the application.");
+ }
+
+ const auto peer = [&](PeerId peerId) -> const Peer& {
+ if (const auto i = peers.find(peerId); i != end(peers)) {
+ return i->second;
+ }
+ static auto empty = Peer{ User() };
+ return empty;
+ };
+ const auto user = [&](int32 userId) -> const User& {
+ if (const auto result = peer(UserPeerId(userId)).user()) {
+ return *result;
+ }
+ static auto empty = User();
+ return empty;
+ };
+ const auto chat = [&](int32 chatId) -> const Chat& {
+ if (const auto result = peer(ChatPeerId(chatId)).chat()) {
+ return *result;
+ }
+ static auto empty = Chat();
+ return empty;
+ };
+
+ auto values = std::vector>{
+ { "ID", SerializeString(NumberToString(message.id)) },
+ { "Date", SerializeString(FormatDateTime(message.date)) },
+ { "Edited", SerializeString(FormatDateTime(message.edited)) },
+ };
+ const auto pushBare = [&](
+ const QByteArray &key,
+ const QByteArray &value) {
+ values.emplace_back(key, value);
+ };
+ const auto push = [&](const QByteArray &key, const QByteArray &value) {
+ if (!value.isEmpty()) {
+ pushBare(key, SerializeString(value));
+ }
+ };
+ const auto wrapPeerName = [&](PeerId peerId) {
+ const auto result = peer(peerId).name();
+ return result.isEmpty() ? QByteArray("(deleted peer)") : result;
+ };
+ const auto wrapUserName = [&](int32 userId) {
+ const auto result = user(userId).name();
+ return result.isEmpty()
+ ? QByteArray("Deleted Account")
+ : SerializeString(result);
+ };
+ const auto pushFrom = [&](const QByteArray &label = "From") {
+ if (message.fromId) {
+ push(label, wrapUserName(message.fromId));
+ }
+ };
+ const auto pushReplyToMsgId = [&](
+ const QByteArray &label = "Reply to message") {
+ if (message.replyToMsgId) {
+ push(label, "ID-" + NumberToString(message.replyToMsgId));
+ }
+ };
+ const auto wrapList = [&](const std::vector &values) {
+ const auto count = values.size();
+ if (count == 1) {
+ return values[0];
+ } else if (count > 1) {
+ auto result = values[0];
+ for (auto i = 1; i != count - 1; ++i) {
+ result += ", " + values[i];
+ }
+ return result + " and " + values[count - 1];
+ }
+ return QByteArray();
+ };
+ const auto wrapUserNames = [&](const std::vector &data) {
+ auto list = std::vector();
+ for (const auto userId : data) {
+ list.push_back(wrapUserName(userId));
+ }
+ return wrapList(list);
+ };
+ const auto pushActor = [&] {
+ pushFrom("Actor");
+ };
+ const auto pushAction = [&](const QByteArray &action) {
+ push("Action", action);
+ };
+ const auto pushTTL = [&](
+ const QByteArray &label = "Self destruct period") {
+ if (const auto ttl = message.media.ttl) {
+ push(label, NumberToString(ttl) + " sec.");
+ }
+ };
+
+ using SkipReason = Data::File::SkipReason;
+ const auto formatPath = [&](
+ const Data::File &file,
+ const QByteArray &label,
+ const QByteArray &name = QByteArray()) {
+ Expects(!file.relativePath.isEmpty()
+ || file.skipReason != SkipReason::None);
+
+ const auto pre = name.isEmpty()
+ ? QByteArray()
+ : SerializeString(name + ' ');
+ switch (file.skipReason) {
+ case SkipReason::Unavailable:
+ return pre + "(" + label + " unavailable, "
+ "please try again later)";
+ case SkipReason::FileSize:
+ return pre + "(" + label + " exceeds maximum size. "
+ "Change data exporting settings to download.)";
+ case SkipReason::FileType:
+ return pre + "(" + label + " not included. "
+ "Change data exporting settings to download.)";
+ case SkipReason::None: return SerializeLink(
+ FormatFilePath(file),
+ relativePath(file.relativePath));
+ }
+ Unexpected("Skip reason while writing file path.");
+ };
+ const auto pushPath = [&](
+ const Data::File &file,
+ const QByteArray &label,
+ const QByteArray &name = QByteArray()) {
+ pushBare(label, formatPath(file, label, name));
+ };
+ const auto pushPhoto = [&](const Image &image) {
+ pushPath(image.file, "Photo");
+ if (image.width && image.height) {
+ push("Width", NumberToString(image.width));
+ push("Height", NumberToString(image.height));
+ }
+ };
+ const auto wrapReplyToLink = [&](const QByteArray &text) {
+ return ""
+ + text + "";
+ };
+
+ const auto serviceFrom = wrapUserName(message.fromId);
+ const auto serviceText = message.action.content.match(
+ [&](const ActionChatCreate &data) {
+ return serviceFrom
+ + " created group «" + data.title + "»"
+ + (data.userIds.empty()
+ ? QByteArray()
+ : " with members " + wrapUserNames(data.userIds));
+ }, [&](const ActionChatEditTitle &data) {
+ return serviceFrom
+ + " changed group title to «" + data.title + "»";
+ }, [&](const ActionChatEditPhoto &data) {
+ return serviceFrom
+ + " changed group photo";
+ }, [&](const ActionChatDeletePhoto &data) {
+ return serviceFrom
+ + " deleted group photo";
+ }, [&](const ActionChatAddUser &data) {
+ return serviceFrom
+ + " invited "
+ + wrapUserNames(data.userIds);
+ }, [&](const ActionChatDeleteUser &data) {
+ return serviceFrom
+ + " removed "
+ + wrapUserName(data.userId);
+ }, [&](const ActionChatJoinedByLink &data) {
+ return serviceFrom
+ + " joined group by link from "
+ + wrapUserName(data.inviterId);
+ }, [&](const ActionChannelCreate &data) {
+ return "Channel «" + data.title + "» created";
+ }, [&](const ActionChatMigrateTo &data) {
+ return serviceFrom
+ + " converted this group to a supergroup";
+ }, [&](const ActionChannelMigrateFrom &data) {
+ return serviceFrom
+ + " converted a basic group to this supergroup "
+ + "«" + data.title + "»";
+ }, [&](const ActionPinMessage &data) {
+ return serviceFrom
+ + " pinned "
+ + wrapReplyToLink("this message");
+ }, [&](const ActionHistoryClear &data) {
+ return QByteArray("History cleared");
+ }, [&](const ActionGameScore &data) {
+ return serviceFrom
+ + " scored "
+ + NumberToString(data.score)
+ + " in "
+ + wrapReplyToLink("this game");
+ }, [&](const ActionPaymentSent &data) {
+ return "You have successfully transferred "
+ + FormatMoneyAmount(data.amount, data.currency)
+ + " for "
+ + wrapReplyToLink("this invoice");
+ }, [&](const ActionPhoneCall &data) {
+ return QByteArray();
+ }, [&](const ActionScreenshotTaken &data) {
+ return serviceFrom + " took a screenshot";
+ }, [&](const ActionCustomAction &data) {
+ return data.message;
+ }, [&](const ActionBotAllowed &data) {
+ return "You allowed this bot to message you when you logged in on "
+ + data.domain;
+ }, [&](const ActionSecureValuesSent &data) {
+ auto list = std::vector();
+ for (const auto type : data.types) {
+ list.push_back([&] {
+ using Type = ActionSecureValuesSent::Type;
+ switch (type) {
+ case Type::PersonalDetails: return "Personal details";
+ case Type::Passport: return "Passport";
+ case Type::DriverLicense: return "Driver license";
+ case Type::IdentityCard: return "Identity card";
+ case Type::InternalPassport: return "Internal passport";
+ case Type::Address: return "Address information";
+ case Type::UtilityBill: return "Utility bill";
+ case Type::BankStatement: return "Bank statement";
+ case Type::RentalAgreement: return "Rental agreement";
+ case Type::PassportRegistration:
+ return "Passport registration";
+ case Type::TemporaryRegistration:
+ return "Temporary registration";
+ case Type::Phone: return "Phone number";
+ case Type::Email: return "Email";
+ }
+ return "";
+ }());
+ }
+ return "You have sent the following documents: " + wrapList(list);
+ }, [](const base::none_type &) { return QByteArray(); });
+
+ if (!serviceText.isEmpty()) {
+ const auto &content = message.action.content;
+ const auto photo = content.is()
+ ? &content.get_unchecked().photo
+ : nullptr;
+ return pushServiceMessage(
+ message.id,
+ dialog,
+ basePath,
+ serviceText,
+ photo);
+ }
+
+ if (!message.action.content) {
+ pushFrom();
+ push("Author", message.signature);
+ if (message.forwardedFromId) {
+ push("Forwarded from", wrapPeerName(message.forwardedFromId));
+ }
+ if (message.savedFromChatId) {
+ push("Saved from", wrapPeerName(message.savedFromChatId));
+ }
+ pushReplyToMsgId();
+ if (message.viaBotId) {
+ push("Via", user(message.viaBotId).username);
+ }
+ }
+
+ message.media.content.match([&](const Photo &photo) {
+ pushPhoto(photo.image);
+ pushTTL();
+ }, [&](const Document &data) {
+ const auto pushMyPath = [&](const QByteArray &label) {
+ return pushPath(data.file, label);
+ };
+ if (data.isSticker) {
+ pushMyPath("Sticker");
+ push("Emoji", data.stickerEmoji);
+ } else if (data.isVideoMessage) {
+ pushMyPath("Video message");
+ } else if (data.isVoiceMessage) {
+ pushMyPath("Voice message");
+ } else if (data.isAnimated) {
+ pushMyPath("Animation");
+ } else if (data.isVideoFile) {
+ pushMyPath("Video file");
+ } else if (data.isAudioFile) {
+ pushMyPath("Audio file");
+ push("Performer", data.songPerformer);
+ push("Title", data.songTitle);
+ } else {
+ pushMyPath("File");
+ }
+ if (!data.isSticker) {
+ push("Mime type", data.mime);
+ }
+ if (data.duration) {
+ push("Duration", NumberToString(data.duration) + " sec.");
+ }
+ if (data.width && data.height) {
+ push("Width", NumberToString(data.width));
+ push("Height", NumberToString(data.height));
+ }
+ pushTTL();
+ }, [&](const SharedContact &data) {
+ pushBare("Contact information", SerializeBlockquote({
+ { "First name", data.info.firstName },
+ { "Last name", data.info.lastName },
+ { "Phone number", FormatPhoneNumber(data.info.phoneNumber) },
+ { "vCard", (data.vcard.content.isEmpty()
+ ? QByteArray()
+ : formatPath(data.vcard, "vCard")) }
+ }));
+ }, [&](const GeoPoint &data) {
+ pushBare("Location", data.valid ? SerializeBlockquote({
+ { "Latitude", NumberToString(data.latitude) },
+ { "Longitude", NumberToString(data.longitude) },
+ }) : QByteArray("(empty value)"));
+ pushTTL("Live location period");
+ }, [&](const Venue &data) {
+ push("Place name", data.title);
+ push("Address", data.address);
+ if (data.point.valid) {
+ pushBare("Location", SerializeBlockquote({
+ { "Latitude", NumberToString(data.point.latitude) },
+ { "Longitude", NumberToString(data.point.longitude) },
+ }));
+ }
+ }, [&](const Game &data) {
+ push("Game", data.title);
+ push("Description", data.description);
+ if (data.botId != 0 && !data.shortName.isEmpty()) {
+ const auto bot = user(data.botId);
+ if (bot.isBot && !bot.username.isEmpty()) {
+ push("Link", internalLinksDomain.toUtf8()
+ + bot.username
+ + "?game="
+ + data.shortName);
+ }
+ }
+ }, [&](const Invoice &data) {
+ pushBare("Invoice", SerializeBlockquote({
+ { "Title", data.title },
+ { "Description", data.description },
+ {
+ "Amount",
+ Data::FormatMoneyAmount(data.amount, data.currency)
+ },
+ { "Receipt message", (data.receiptMsgId
+ ? "ID-" + NumberToString(data.receiptMsgId)
+ : QByteArray()) }
+ }));
+ }, [](const UnsupportedMedia &data) {
+ Unexpected("Unsupported message.");
+ }, [](const base::none_type &) {});
+
+ auto value = JoinList(QByteArray(), ranges::view::all(
+ message.text
+ ) | ranges::view::transform([&](const Data::TextPart &part) {
+ const auto text = SerializeString(part.text);
+ using Type = Data::TextPart::Type;
+ switch (part.type) {
+ case Type::Text: return text;
+ case Type::Unknown: return text;
+ case Type::Mention:
+ return "" + text + "";
+ case Type::Hashtag: return "" + text + "";
+ case Type::BotCommand: return "" + text + "";
+ case Type::Url: return "" + text + "";
+ case Type::Email: return "" + text + "";
+ case Type::Bold: return "" + text + "";
+ case Type::Italic: return "" + text + "";
+ case Type::Code: return "" + text + "
";
+ case Type::Pre: return "" + text + "
";
+ case Type::TextUrl: return "" + text + "";
+ case Type::MentionName: return "" + text + "";
+ case Type::Phone: return "" + text + "";
+ case Type::Cashtag: return "" + text + "";
+ }
+ Unexpected("Type in text entities serialization.");
+ }) | ranges::to_vector);
+ pushBare("Text", value);
+
+ return SerializeKeyValue(std::move(values));
+}
+
Result HtmlWriter::Wrap::close() {
if (!std::exchange(_closed, true) && !_file.empty()) {
auto block = QByteArray();
@@ -1111,7 +1213,11 @@ Result HtmlWriter::writePreparedPersonal(
userpic.largeLink = userpicPath.isEmpty()
? QString()
: userpicsFilePath();
- userpic.imageLink = writeUserpicThumb(userpicPath, userpic, "_info");
+ userpic.imageLink = WriteUserpicThumb(
+ _settings.path,
+ userpicPath,
+ userpic,
+ "_info");
userpic.firstName = info.firstName;
userpic.lastName = info.lastName;
@@ -1153,18 +1259,6 @@ Result HtmlWriter::writePreparedPersonal(
return _summary->writeBlock(block);
}
-QString HtmlWriter::writeUserpicThumb(
- const QString &largePath,
- const UserpicData &userpic,
- const QString &postfix) {
- return Data::WriteImageThumb(
- _settings.path,
- largePath,
- userpic.pixelSize * 2,
- userpic.pixelSize * 2,
- postfix);
-}
-
Result HtmlWriter::writeUserpicsStart(const Data::UserpicsInfo &data) {
Expects(_summary != nullptr);
Expects(_userpics == nullptr);
@@ -1220,7 +1314,7 @@ Result HtmlWriter::writeUserpicsSlice(const Data::UserpicsSlice &data) {
Unexpected("Skip reason while writing photo path.");
}();
const auto &path = userpic.image.file.relativePath;
- data.imageLink = writeUserpicThumb(path, data);
+ data.imageLink = WriteUserpicThumb(_settings.path, path, data);
data.firstName = path.toUtf8();
block.append(_userpics->pushListEntry(
data,
@@ -1592,6 +1686,8 @@ Result HtmlWriter::writeChatStart(const Data::DialogInfo &data) {
const auto number = Data::NumberToString(++_dialogIndex, digits, '0');
_chat = fileWithRelativePath(data.relativePath + messagesFile(0));
_messagesCount = 0;
+ _dateMessageId = 0;
+ _lastMessageDate = 0;
_dialog = data;
return Result::Success();
}
@@ -1620,19 +1716,25 @@ Result HtmlWriter::writeChatSlice(const Data::MessagesSlice &data) {
}
}
- auto list = std::vector();
- list.reserve(data.list.size());
+ auto block = QByteArray();
for (const auto &message : data.list) {
- list.push_back(SerializeMessage(
- [&](QString path) { return _chat->relativePath(path); },
+ const auto date = message.date;
+ if (DisplayDate(date, _lastMessageDate)) {
+ block.append(_chat->pushServiceMessage(
+ --_dateMessageId,
+ _dialog,
+ _settings.path,
+ FormatDateText(date)));
+ }
+ block.append(_chat->pushMessage(
message,
+ _dialog,
+ _settings.path,
data.peers,
_environment.internalLinksDomain));
+ _lastMessageDate = date;
}
- const auto full = _chat->empty()
- ? JoinList(kLineBreak, list)
- : kLineBreak + JoinList(kLineBreak, list);
- return _chat->writeBlock(full);
+ return _chat->writeBlock(block);
}
Result HtmlWriter::writeChatEnd() {
diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h
index 9a04b7cac..6a8cfc2b6 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.h
+++ b/Telegram/SourceFiles/export/output/export_output_html.h
@@ -128,11 +128,6 @@ private:
const QString &userpicPath);
void pushUserpicsSection();
- [[nodiscard]] QString writeUserpicThumb(
- const QString &largePath,
- const UserpicData &userpic,
- const QString &postfix = "_thumb");
-
[[nodiscard]] QString userpicsFilePath() const;
Settings _settings;
@@ -158,6 +153,8 @@ private:
Data::DialogInfo _dialog;
int _messagesCount = 0;
+ TimeId _lastMessageDate = 0;
+ int _dateMessageId = 0;
std::unique_ptr _chats;
std::unique_ptr _chat;