Skip to content

Commit

Permalink
Add reactions to export chat history
Browse files Browse the repository at this point in the history
  • Loading branch information
BohdanTkachenko committed Aug 31, 2024
1 parent 9e1d9ee commit 19c06a7
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 54 deletions.
42 changes: 42 additions & 0 deletions Telegram/Resources/export_html/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -582,3 +582,45 @@ 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 .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;
}
72 changes: 72 additions & 0 deletions Telegram/SourceFiles/export/data/export_data_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,75 @@ std::vector<TextPart> 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";
}
Unexpected("Type in Reaction::Type.");
}

Utf8String Reaction::Id(const Reaction &reaction) {
Utf8String id;
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) {
Reaction result;
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 MTPDreactionEmpty &data) {
result.type = Reaction::Type::Empty;
});
return result;
}

std::vector<Reaction> ParseReactions(const MTPMessageReactions &data) {
std::map<QString, Reaction> reactionsMap;
std::vector<Utf8String> reactionsOrder;
for (const auto &single : data.data().vresults().v) {
Reaction reaction = ParseReaction(single.data().vreaction());
reaction.count = single.data().vcount().v;
Utf8String 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()) {
for (const auto &single : data.data().vrecent_reactions().value_or_empty()) {
Reaction reaction = ParseReaction(single.data().vreaction());
Utf8String 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<Reaction> 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;
Expand Down Expand Up @@ -1688,6 +1757,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,
Expand Down
24 changes: 24 additions & 0 deletions Telegram/SourceFiles/export/data/export_data_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,29 @@ struct TextPart {
}
};

struct Reaction {
enum class Type {
Empty,
Emoji,
CustomEmoji,
};

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

struct MessageId {
ChannelId channelId;
int32 msgId = 0;
Expand Down Expand Up @@ -744,6 +767,7 @@ struct Message {
int32 replyToMsgId = 0;
PeerId replyToPeerId = 0;
std::vector<TextPart> text;
std::vector<Reaction> reactions;
Media media;
ServiceAction action;
bool out = false;
Expand Down
81 changes: 52 additions & 29 deletions Telegram/SourceFiles/export/export_api_wrap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
}
}

Expand Down Expand Up @@ -1803,38 +1812,52 @@ Data::FileOrigin ApiWrap::currentFileMessageOrigin() const {
return result;
}

bool ApiWrap::renderCustomEmoji(QByteArray *data) {
if (const auto id = data->toULongLong()) {
const auto i = _resolvedCustomEmoji.find(id);
if (i == end(_resolvedCustomEmoji)) {
*data = 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) {
*data = Data::TextPart::UnavailableEmoji();
} else if (file.skipReason == SkipReason::FileType
|| file.skipReason == SkipReason::FileSize) {
*data = QByteArray();
} else {
*data = file.relativePath.toUtf8();
}
}
}
return true;
}

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();
}
}
if (!renderCustomEmoji(&part.additional)) {
return false;
}
}
}
for (auto &reaction : message.reactions) {
if (reaction.type == Data::Reaction::Type::CustomEmoji) {
if (!renderCustomEmoji(&reaction.documentId)) {
return false;
}
}
}
Expand Down
1 change: 1 addition & 0 deletions Telegram/SourceFiles/export/export_api_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ class ApiWrap {
void resolveCustomEmoji();
void loadMessagesFiles(Data::MessagesSlice &&slice);
void loadNextMessageFile();
bool renderCustomEmoji(QByteArray *data);
bool messageCustomEmojiReady(Data::Message &message);
bool loadMessageFileProgress(FileProgress value);
void loadMessageFileDone(const QString &relativePath);
Expand Down
87 changes: 78 additions & 9 deletions Telegram/SourceFiles/export/output/export_output_html.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,21 @@ QByteArray JoinList(
return result;
}

QByteArray FormatCustomEmoji(
const Data::Utf8String &custom_emoji,
const QByteArray &text,
const QString &relativeLinkBase) {
return (custom_emoji.isEmpty()
? "<a href=\"\" onclick=\"return ShowNotLoadedEmoji();\">"
: (custom_emoji == Data::TextPart::UnavailableEmoji())
? "<a href=\"\" onclick=\"return ShowNotAvailableEmoji();\">"
: ("<a href = \""
+ (relativeLinkBase + custom_emoji).toUtf8()
+ "\">"))
+ text
+ "</a>";
}

QByteArray FormatText(
const std::vector<Data::TextPart> &data,
const QString &internalLinksDomain,
Expand Down Expand Up @@ -287,15 +302,8 @@ QByteArray FormatText(
"onclick=\"ShowSpoiler(this)\">"
"<span aria-hidden=\"true\">"
+ text + "</span></span>";
case Type::CustomEmoji: return (part.additional.isEmpty()
? "<a href=\"\" onclick=\"return ShowNotLoadedEmoji();\">"
: (part.additional == Data::TextPart::UnavailableEmoji())
? "<a href=\"\" onclick=\"return ShowNotAvailableEmoji();\">"
: ("<a href = \""
+ (relativeLinkBase + part.additional).toUtf8()
+ "\">"))
+ text
+ "</a>";
case Type::CustomEmoji: return FormatCustomEmoji(
part.additional, text, relativeLinkBase);
}
Unexpected("Type in text entities serialization.");
}) | ranges::to_vector);
Expand Down Expand Up @@ -1516,6 +1524,67 @@ 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;
}
}

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,
"(custom emoji)",
_base));
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());

Expand Down
Loading

0 comments on commit 19c06a7

Please sign in to comment.