Add reactions to export chat history (#28252)
Co-authored-by: 23rd <23rd@vivaldi.net>
This commit is contained in:
parent
91f5c72cf0
commit
a970fe93c1
|
@ -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;
|
||||
}
|
|
@ -325,6 +325,80 @@ 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";
|
||||
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<Reaction> ParseReactions(const MTPMessageReactions &data) {
|
||||
auto reactionsMap = std::map<QString, Reaction>();
|
||||
auto reactionsOrder = std::vector<Utf8String>();
|
||||
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<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;
|
||||
|
@ -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,
|
||||
|
|
|
@ -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> recent;
|
||||
};
|
||||
|
||||
struct MessageId {
|
||||
ChannelId channelId;
|
||||
int32 msgId = 0;
|
||||
|
@ -775,6 +799,7 @@ struct Message {
|
|||
int32 replyToMsgId = 0;
|
||||
PeerId replyToPeerId = 0;
|
||||
std::vector<TextPart> text;
|
||||
std::vector<Reaction> reactions;
|
||||
Media media;
|
||||
ServiceAction action;
|
||||
bool out = false;
|
||||
|
|
|
@ -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<QByteArray> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -183,6 +183,7 @@ private:
|
|||
void resolveCustomEmoji();
|
||||
void loadMessagesFiles(Data::MessagesSlice &&slice);
|
||||
void loadNextMessageFile();
|
||||
std::optional<QByteArray> getCustomEmoji(QByteArray &data);
|
||||
bool messageCustomEmojiReady(Data::Message &message);
|
||||
bool loadMessageFileProgress(FileProgress value);
|
||||
void loadMessageFileDone(const QString &relativePath);
|
||||
|
|
|
@ -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()
|
||||
? "<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,
|
||||
|
@ -288,15 +303,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);
|
||||
|
@ -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());
|
||||
|
||||
|
|
|
@ -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<decltype(value)>;
|
||||
if constexpr (std::is_same_v<V, bool>) {
|
||||
|
@ -297,22 +308,7 @@ QByteArray SerializeMessage(
|
|||
} else if constexpr (std::is_arithmetic_v<V>) {
|
||||
pushBare(key, Data::NumberToString(value));
|
||||
} else if constexpr (std::is_same_v<V, PeerId>) {
|
||||
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<std::pair<QByteArray, QByteArray>>();
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue