Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reactions to export chat history #28252

Merged
merged 22 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
19c06a7
Add reactions to export chat history
BohdanTkachenko Jun 20, 2024
281e08a
Add MTPDreactionPaid support
BohdanTkachenko Sep 1, 2024
fbb5e29
Update Telegram/SourceFiles/export/data/export_data_types.cpp
BohdanTkachenko Sep 21, 2024
e2bd535
Update Telegram/SourceFiles/export/data/export_data_types.cpp
BohdanTkachenko Sep 21, 2024
7c4ba74
Update Telegram/SourceFiles/export/data/export_data_types.cpp
BohdanTkachenko Sep 21, 2024
9d745d9
Update Telegram/SourceFiles/export/data/export_data_types.cpp
BohdanTkachenko Sep 21, 2024
6f03f77
Update Telegram/SourceFiles/export/data/export_data_types.cpp
BohdanTkachenko Sep 21, 2024
0885b59
Update Telegram/SourceFiles/export/output/export_output_html.cpp
BohdanTkachenko Sep 21, 2024
f75e00e
Update Telegram/SourceFiles/export/data/export_data_types.cpp
BohdanTkachenko Sep 21, 2024
b6b0cab
Update Telegram/SourceFiles/export/output/export_output_html.cpp
BohdanTkachenko Sep 21, 2024
4a1eb9d
Update Telegram/SourceFiles/export/data/export_data_types.cpp
BohdanTkachenko Sep 21, 2024
6708962
Rename renderCustomEmoji to getCustomEmoji
BohdanTkachenko Sep 21, 2024
bfff0b1
Fix HTML exporting being stuck
BohdanTkachenko Sep 21, 2024
2a34366
Fix rendering of paid reactions in HTML export
BohdanTkachenko Sep 21, 2024
0c42af8
Use waving hand emoji for all custom emojis reactions
BohdanTkachenko Sep 21, 2024
3b47c76
Update Telegram/SourceFiles/export/output/export_output_html.cpp
23rd Sep 30, 2024
58a4e31
Update Telegram/SourceFiles/export/output/export_output_html.cpp
23rd Sep 30, 2024
64d63bb
Update Telegram/SourceFiles/export/output/export_output_html.cpp
23rd Sep 30, 2024
309d0d1
Update Telegram/SourceFiles/export/output/export_output_json.cpp
23rd Sep 30, 2024
dab5d6d
Update Telegram/SourceFiles/export/output/export_output_json.cpp
23rd Sep 30, 2024
6324970
Update Telegram/SourceFiles/export/output/export_output_json.cpp
23rd Sep 30, 2024
439cf6a
Update Telegram/SourceFiles/export/output/export_output_json.cpp
23rd Sep 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
74 changes: 74 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,77 @@ 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";
BohdanTkachenko marked this conversation as resolved.
Show resolved Hide resolved
}
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;
BohdanTkachenko marked this conversation as resolved.
Show resolved Hide resolved
}
return Reaction::TypeToString(reaction) + id;
}

Reaction ParseReaction(const MTPReaction& reaction) {
Reaction result;
BohdanTkachenko marked this conversation as resolved.
Show resolved Hide resolved
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<Reaction> ParseReactions(const MTPMessageReactions &data) {
std::map<QString, Reaction> reactionsMap;
std::vector<Utf8String> reactionsOrder;
BohdanTkachenko marked this conversation as resolved.
Show resolved Hide resolved
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()) {
BohdanTkachenko marked this conversation as resolved.
Show resolved Hide resolved
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) {
BohdanTkachenko marked this conversation as resolved.
Show resolved Hide resolved
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 +1759,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
25 changes: 25 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,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> recent;
};

struct MessageId {
ChannelId channelId;
int32 msgId = 0;
Expand Down Expand Up @@ -744,6 +768,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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QByteArray *data

Can’t say I like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to return an optional value instead and avoid passing data by pointer directly. Does it look better now?

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>";
BohdanTkachenko marked this conversation as resolved.
Show resolved Hide resolved
}

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);
BohdanTkachenko marked this conversation as resolved.
Show resolved Hide resolved
}
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) {
23rd marked this conversation as resolved.
Show resolved Hide resolved
QByteArray reactionClass = "reaction";
for (const auto& recent : reaction.recent) {
23rd marked this conversation as resolved.
Show resolved Hide resolved
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) {
23rd marked this conversation as resolved.
Show resolved Hide resolved
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
Loading