diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css
index 102f5f3a5d..f9727d68ac 100644
--- a/Telegram/Resources/export_html/css/style.css
+++ b/Telegram/Resources/export_html/css/style.css
@@ -582,3 +582,55 @@ div.toast_shown {
.bot_button_column_separator {
width: 2px
}
+
+.reactions {
+ margin: 5px 0;
+}
+
+.reactions .reaction {
+ display: inline-flex;
+ height: 20px;
+ border-radius: 15px;
+ background-color: #e8f5fc;
+ color: #168acd;
+ font-weight: bold;
+ margin-bottom: 5px;
+}
+
+.reactions .reaction.active {
+ background-color: #40a6e2;
+ color: #fff;
+}
+
+.reactions .reaction.paid {
+ background-color: #fdf6e1;
+ color: #c58523;
+}
+
+.reactions .reaction.active.paid {
+ background-color: #ecae0a;
+ color: #fdf6e1;
+}
+
+.reactions .reaction .emoji {
+ line-height: 20px;
+ margin: 0 5px;
+ font-size: 15px;
+}
+
+.reactions .reaction .userpic:not(:first-child) {
+ margin-left: -8px;
+}
+
+.reactions .reaction .userpic {
+ display: inline-block;
+}
+
+.reactions .reaction .userpic .initials {
+ font-size: 8px;
+}
+
+.reactions .reaction .count {
+ margin-right: 8px;
+ line-height: 20px;
+}
\ No newline at end of file
diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp
index 5f95e671b3..bbb363dc5e 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.cpp
+++ b/Telegram/SourceFiles/export/data/export_data_types.cpp
@@ -325,6 +325,80 @@ std::vector ParseText(
return result;
}
+Utf8String Reaction::TypeToString(const Reaction &reaction) {
+ switch (reaction.type) {
+ case Reaction::Type::Empty: return "empty";
+ case Reaction::Type::Emoji: return "emoji";
+ case Reaction::Type::CustomEmoji: return "custom_emoji";
+ case Reaction::Type::Paid: return "paid";
+ }
+ Unexpected("Type in Reaction::Type.");
+}
+
+Utf8String Reaction::Id(const Reaction &reaction) {
+ auto id = Utf8String();
+ switch (reaction.type) {
+ case Reaction::Type::Emoji:
+ id = reaction.emoji.toUtf8();
+ break;
+ case Reaction::Type::CustomEmoji:
+ id = reaction.documentId;
+ break;
+ }
+ return Reaction::TypeToString(reaction) + id;
+}
+
+Reaction ParseReaction(const MTPReaction& reaction) {
+ auto result = Reaction();
+ reaction.match([&](const MTPDreactionEmoji &data) {
+ result.type = Reaction::Type::Emoji;
+ result.emoji = qs(data.vemoticon());
+ }, [&](const MTPDreactionCustomEmoji &data) {
+ result.type = Reaction::Type::CustomEmoji;
+ result.documentId = NumberToString(data.vdocument_id().v);
+ }, [&](const MTPDreactionPaid &data) {
+ result.type = Reaction::Type::Paid;
+ }, [&](const MTPDreactionEmpty &data) {
+ result.type = Reaction::Type::Empty;
+ });
+ return result;
+}
+
+std::vector ParseReactions(const MTPMessageReactions &data) {
+ auto reactionsMap = std::map();
+ auto reactionsOrder = std::vector();
+ for (const auto &single : data.data().vresults().v) {
+ auto reaction = ParseReaction(single.data().vreaction());
+ reaction.count = single.data().vcount().v;
+ auto id = Reaction::Id(reaction);
+ auto const &[_, inserted] = reactionsMap.try_emplace(id, reaction);
+ if (inserted) {
+ reactionsOrder.push_back(id);
+ }
+ }
+ if (data.data().vrecent_reactions().has_value()) {
+ if (const auto list = data.data().vrecent_reactions()) {
+ for (const auto &single : list->v) {
+ auto reaction = ParseReaction(single.data().vreaction());
+ auto id = Reaction::Id(reaction);
+ auto const &[it, inserted] = reactionsMap.try_emplace(id, reaction);
+ if (inserted) {
+ reactionsOrder.push_back(id);
+ }
+ it->second.recent.push_back({
+ .peerId = ParsePeerId(single.data().vpeer_id()),
+ .date = single.data().vdate().v,
+ });
+ }
+ }
+ }
+ std::vector results;
+ for (const auto &id : reactionsOrder) {
+ results.push_back(reactionsMap[id]);
+ }
+ return results;
+}
+
Utf8String FillLeft(const Utf8String &data, int length, char filler) {
if (length <= data.size()) {
return data;
@@ -1739,6 +1813,9 @@ Message ParseMessage(
result.text = ParseText(
data.vmessage(),
data.ventities().value_or_empty());
+ if (data.vreactions().has_value()) {
+ result.reactions = ParseReactions(*data.vreactions());
+ }
}, [&](const MTPDmessageService &data) {
result.action = ParseServiceAction(
context,
diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h
index ce2d97842d..7d6c2e6681 100644
--- a/Telegram/SourceFiles/export/data/export_data_types.h
+++ b/Telegram/SourceFiles/export/data/export_data_types.h
@@ -702,6 +702,30 @@ struct TextPart {
}
};
+struct Reaction {
+ enum class Type {
+ Empty,
+ Emoji,
+ CustomEmoji,
+ Paid,
+ };
+
+ static Utf8String TypeToString(const Reaction &);
+
+ static Utf8String Id(const Reaction &);
+
+ struct Recent {
+ PeerId peerId = 0;
+ TimeId date = 0;
+ };
+
+ Type type;
+ QString emoji;
+ Utf8String documentId;
+ uint32 count = 0;
+ std::vector recent;
+};
+
struct MessageId {
ChannelId channelId;
int32 msgId = 0;
@@ -775,6 +799,7 @@ struct Message {
int32 replyToMsgId = 0;
PeerId replyToPeerId = 0;
std::vector text;
+ std::vector reactions;
Media media;
ServiceAction action;
bool out = false;
diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp
index 69861015c3..0d408f142b 100644
--- a/Telegram/SourceFiles/export/export_api_wrap.cpp
+++ b/Telegram/SourceFiles/export/export_api_wrap.cpp
@@ -1726,6 +1726,15 @@ void ApiWrap::collectMessagesCustomEmoji(const Data::MessagesSlice &slice) {
}
}
}
+ for (const auto &reaction : message.reactions) {
+ if (reaction.type == Data::Reaction::Type::CustomEmoji) {
+ if (const auto id = reaction.documentId.toULongLong()) {
+ if (!_resolvedCustomEmoji.contains(id)) {
+ _unresolvedCustomEmoji.emplace(id);
+ }
+ }
+ }
+ }
}
}
@@ -1803,38 +1812,57 @@ Data::FileOrigin ApiWrap::currentFileMessageOrigin() const {
return result;
}
+std::optional ApiWrap::getCustomEmoji(QByteArray &data) {
+ if (const auto id = data.toULongLong()) {
+ const auto i = _resolvedCustomEmoji.find(id);
+ if (i == end(_resolvedCustomEmoji)) {
+ return Data::TextPart::UnavailableEmoji();
+ }
+ auto &file = i->second.file;
+ const auto fileProgress = [=](FileProgress value) {
+ return loadMessageEmojiProgress(value);
+ };
+ const auto ready = processFileLoad(
+ file,
+ { .customEmojiId = id },
+ fileProgress,
+ [=](const QString &path) {
+ loadMessageEmojiDone(id, path);
+ });
+ if (!ready) {
+ return std::nullopt;
+ }
+ using SkipReason = Data::File::SkipReason;
+ if (file.skipReason == SkipReason::Unavailable) {
+ return Data::TextPart::UnavailableEmoji();
+ } else if (file.skipReason == SkipReason::FileType
+ || file.skipReason == SkipReason::FileSize) {
+ return QByteArray();
+ } else {
+ return file.relativePath.toUtf8();
+ }
+ }
+ return data;
+}
+
bool ApiWrap::messageCustomEmojiReady(Data::Message &message) {
for (auto &part : message.text) {
if (part.type == Data::TextPart::Type::CustomEmoji) {
- if (const auto id = part.additional.toULongLong()) {
- const auto i = _resolvedCustomEmoji.find(id);
- if (i == end(_resolvedCustomEmoji)) {
- part.additional = Data::TextPart::UnavailableEmoji();
- } else {
- auto &file = i->second.file;
- const auto fileProgress = [=](FileProgress value) {
- return loadMessageEmojiProgress(value);
- };
- const auto ready = processFileLoad(
- file,
- { .customEmojiId = id },
- fileProgress,
- [=](const QString &path) {
- loadMessageEmojiDone(id, path);
- });
- if (!ready) {
- return false;
- }
- using SkipReason = Data::File::SkipReason;
- if (file.skipReason == SkipReason::Unavailable) {
- part.additional = Data::TextPart::UnavailableEmoji();
- } else if (file.skipReason == SkipReason::FileType
- || file.skipReason == SkipReason::FileSize) {
- part.additional = QByteArray();
- } else {
- part.additional = file.relativePath.toUtf8();
- }
- }
+ auto data = getCustomEmoji(part.additional);
+ if (data.has_value()) {
+ part.additional = *data;
+ } else {
+ return false;
+ }
+ }
+ }
+ for (auto &reaction : message.reactions) {
+ if (reaction.type == Data::Reaction::Type::CustomEmoji) {
+ auto data = getCustomEmoji(reaction.documentId);
+ if (data.has_value()) {
+ reaction.documentId = *data;
+ } else {
+ return false;
}
}
}
diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h
index 4723cd882d..613d7c5881 100644
--- a/Telegram/SourceFiles/export/export_api_wrap.h
+++ b/Telegram/SourceFiles/export/export_api_wrap.h
@@ -183,6 +183,7 @@ private:
void resolveCustomEmoji();
void loadMessagesFiles(Data::MessagesSlice &&slice);
void loadNextMessageFile();
+ std::optional getCustomEmoji(QByteArray &data);
bool messageCustomEmojiReady(Data::Message &message);
bool loadMessageFileProgress(FileProgress value);
void loadMessageFileDone(const QString &relativePath);
diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp
index 00a755bd93..e9b180b40d 100644
--- a/Telegram/SourceFiles/export/output/export_output_html.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_html.cpp
@@ -231,6 +231,21 @@ QByteArray JoinList(
return result;
}
+QByteArray FormatCustomEmoji(
+ const Data::Utf8String &custom_emoji,
+ const QByteArray &text,
+ const QString &relativeLinkBase) {
+ return (custom_emoji.isEmpty()
+ ? ""
+ : (custom_emoji == Data::TextPart::UnavailableEmoji())
+ ? ""
+ : (""))
+ + text
+ + "";
+}
+
QByteArray FormatText(
const std::vector &data,
const QString &internalLinksDomain,
@@ -288,15 +303,8 @@ QByteArray FormatText(
"onclick=\"ShowSpoiler(this)\">"
""
+ text + "";
- case Type::CustomEmoji: return (part.additional.isEmpty()
- ? ""
- : (part.additional == Data::TextPart::UnavailableEmoji())
- ? ""
- : (""))
- + text
- + "";
+ case Type::CustomEmoji:
+ return FormatCustomEmoji(part.additional, text, relativeLinkBase);
}
Unexpected("Type in text entities serialization.");
}) | ranges::to_vector);
@@ -1545,6 +1553,73 @@ auto HtmlWriter::Wrap::pushMessage(
if (showForwardedInfo) {
block.append(popTag());
}
+ if (!message.reactions.empty()) {
+ block.append(pushDiv("reactions"));
+ for (const auto &reaction : message.reactions) {
+ QByteArray reactionClass = "reaction";
+ for (const auto &recent : reaction.recent) {
+ auto peer = peers.peer(recent.peerId);
+ if (peer.user() && peer.user()->isSelf) {
+ reactionClass += " active";
+ break;
+ }
+ }
+ if (reaction.type == Reaction::Type::Paid) {
+ reactionClass += " paid";
+ }
+
+ block.append(pushTag("div", {
+ { "class", reactionClass },
+ }));
+ block.append(pushTag("div", {
+ { "class", "emoji" },
+ }));
+ switch (reaction.type) {
+ case Reaction::Type::Emoji:
+ block.append(SerializeString(reaction.emoji.toUtf8()));
+ break;
+ case Reaction::Type::CustomEmoji:
+ block.append(FormatCustomEmoji(
+ reaction.documentId,
+ "\U0001F44B",
+ _base));
+ break;
+ case Reaction::Type::Paid:
+ block.append(SerializeString("\u2B50"));
+ break;
+ }
+ block.append(popTag());
+ if (!reaction.recent.empty()) {
+ block.append(pushTag("div", {
+ { "class", "userpics" },
+ }));
+ for (const auto &recent : reaction.recent) {
+ auto peer = peers.peer(recent.peerId);
+ block.append(pushUserpic(UserpicData({
+ .colorIndex = peer.colorIndex(),
+ .pixelSize = 20,
+ .firstName = peer.user()
+ ? peer.user()->info.firstName
+ : peer.name(),
+ .lastName = peer.user()
+ ? peer.user()->info.lastName
+ : "",
+ .tooltip = peer.name(),
+ })));
+ }
+ block.append(popTag());
+ }
+ if (reaction.recent.empty() || reaction.count > reaction.recent.size()) {
+ block.append(pushTag("div", {
+ { "class", "count" },
+ }));
+ block.append(NumberToString(reaction.count));
+ block.append(popTag());
+ }
+ block.append(popTag());
+ }
+ block.append(popTag());
+ }
block.append(popTag());
block.append(popTag());
diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp
index c7378f392d..4acb12d2e5 100644
--- a/Telegram/SourceFiles/export/output/export_output_json.cpp
+++ b/Telegram/SourceFiles/export/output/export_output_json.cpp
@@ -290,6 +290,17 @@ QByteArray SerializeMessage(
pushBare("edited_unixtime", SerializeDateRaw(message.edited));
}
+ const auto wrapPeerId = [&](PeerId peerId) {
+ if (const auto chat = peerToChat(peerId)) {
+ return SerializeString("chat"
+ + Data::NumberToString(chat.bare));
+ } else if (const auto channel = peerToChannel(peerId)) {
+ return SerializeString("channel"
+ + Data::NumberToString(channel.bare));
+ }
+ return SerializeString("user"
+ + Data::NumberToString(peerToUser(peerId).bare));
+ };
const auto push = [&](const QByteArray &key, const auto &value) {
using V = std::decay_t;
if constexpr (std::is_same_v) {
@@ -297,22 +308,7 @@ QByteArray SerializeMessage(
} else if constexpr (std::is_arithmetic_v) {
pushBare(key, Data::NumberToString(value));
} else if constexpr (std::is_same_v) {
- if (const auto chat = peerToChat(value)) {
- pushBare(
- key,
- SerializeString("chat"
- + Data::NumberToString(chat.bare)));
- } else if (const auto channel = peerToChannel(value)) {
- pushBare(
- key,
- SerializeString("channel"
- + Data::NumberToString(channel.bare)));
- } else {
- pushBare(
- key,
- SerializeString("user"
- + Data::NumberToString(peerToUser(value).bare)));
- }
+ pushBare(key, wrapPeerId(value));
} else {
const auto wrapped = QByteArray(value);
if (!wrapped.isEmpty()) {
@@ -919,6 +915,63 @@ QByteArray SerializeMessage(
pushBare("inline_bot_buttons", SerializeArray(context, rows));
}
+ if (!message.reactions.empty()) {
+ const auto serializeReaction = [&](const Reaction &reaction) {
+ context.nesting.push_back(Context::kObject);
+ const auto guard = gsl::finally([&] { context.nesting.pop_back(); });
+
+ auto pairs = std::vector>();
+ pairs.push_back({
+ "type",
+ SerializeString(Reaction::TypeToString(reaction)),
+ });
+ pairs.push_back({
+ "count",
+ NumberToString(reaction.count),
+ });
+ switch (reaction.type) {
+ case Reaction::Type::Emoji:
+ pairs.push_back({
+ "emoji",
+ SerializeString(reaction.emoji.toUtf8()),
+ });
+ break;
+ case Reaction::Type::CustomEmoji:
+ pairs.push_back({
+ "document_id",
+ SerializeString(reaction.documentId),
+ });
+ break;
+ }
+
+ if (!reaction.recent.empty()) {
+ context.nesting.push_back(Context::kArray);
+ const auto recents = ranges::views::all(
+ reaction.recent
+ ) | ranges::views::transform([&](const Reaction::Recent &recent) {
+ context.nesting.push_back(Context::kArray);
+ const auto guard = gsl::finally([&] { context.nesting.pop_back(); });
+ return SerializeObject(context, {
+ { "from", wrapPeerName(recent.peerId) },
+ { "from_id", wrapPeerId(recent.peerId) },
+ { "date", SerializeDate(recent.date) },
+ });
+ }) | ranges::to_vector;
+ pairs.push_back({"recent", SerializeArray(context, recents)});
+ context.nesting.pop_back();
+ }
+
+ return SerializeObject(context, pairs);
+ };
+
+ context.nesting.push_back(Context::kArray);
+ const auto reactions = ranges::views::all(
+ message.reactions
+ ) | ranges::views::transform(serializeReaction) | ranges::to_vector;
+ pushBare("reactions", SerializeArray(context, reactions));
+ context.nesting.pop_back();
+ }
+
return serialized();
}