mirror of https://github.com/procxx/kepka.git
Track all single-emoji messages.
This commit is contained in:
parent
7d2896dd42
commit
7e4c9f98a6
|
@ -23,20 +23,58 @@ constexpr auto kRefreshTimeout = TimeId(7200);
|
||||||
|
|
||||||
EmojiPack::EmojiPack(not_null<Main::Session*> session) : _session(session) {
|
EmojiPack::EmojiPack(not_null<Main::Session*> session) : _session(session) {
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
|
session->data().itemRemoved(
|
||||||
|
) | rpl::filter([](not_null<const HistoryItem*> item) {
|
||||||
|
return item->isSingleEmoji();
|
||||||
|
}) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
|
||||||
|
remove(item);
|
||||||
|
}, _lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EmojiPack::add(not_null<HistoryItem*> item, const QString &text) {
|
||||||
|
auto length = 0;
|
||||||
|
const auto trimmed = text.trimmed();
|
||||||
|
if (const auto emoji = Ui::Emoji::Find(trimmed, &length)) {
|
||||||
|
if (length == trimmed.size()) {
|
||||||
|
_items[emoji].emplace(item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EmojiPack::remove(not_null<const HistoryItem*> item) {
|
||||||
|
if (!item->isSingleEmoji()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto length = 0;
|
||||||
|
const auto trimmed = item->originalString().trimmed();
|
||||||
|
const auto emoji = Ui::Emoji::Find(trimmed, &length);
|
||||||
|
Assert(emoji != nullptr);
|
||||||
|
Assert(length == trimmed.size());
|
||||||
|
const auto i = _items.find(emoji);
|
||||||
|
Assert(i != end(_items));
|
||||||
|
const auto j = i->second.find(item);
|
||||||
|
Assert(j != end(i->second));
|
||||||
|
i->second.erase(j);
|
||||||
|
if (i->second.empty()) {
|
||||||
|
_items.erase(i);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentData *EmojiPack::stickerForEmoji(not_null<HistoryItem*> item) {
|
DocumentData *EmojiPack::stickerForEmoji(not_null<HistoryItem*> item) {
|
||||||
const auto text = item->originalText().text.trimmed();
|
if (!item->isSingleEmoji()) {
|
||||||
auto length = 0;
|
|
||||||
const auto emoji = Ui::Emoji::Find(text, &length);
|
|
||||||
if (!emoji || length != text.size()) {
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
auto length = 0;
|
||||||
|
const auto trimmed = item->originalString().trimmed();
|
||||||
|
const auto emoji = Ui::Emoji::Find(trimmed, &length);
|
||||||
|
Assert(emoji != nullptr);
|
||||||
|
Assert(length == trimmed.size());
|
||||||
const auto i = _map.find(emoji);
|
const auto i = _map.find(emoji);
|
||||||
if (i != end(_map)) {
|
return (i != end(_map)) ? i->second.get() : nullptr;
|
||||||
return i->second;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmojiPack::refresh() {
|
void EmojiPack::refresh() {
|
||||||
|
@ -46,42 +84,86 @@ void EmojiPack::refresh() {
|
||||||
_requestId = _session->api().request(MTPmessages_GetStickerSet(
|
_requestId = _session->api().request(MTPmessages_GetStickerSet(
|
||||||
MTP_inputStickerSetAnimatedEmoji()
|
MTP_inputStickerSetAnimatedEmoji()
|
||||||
)).done([=](const MTPmessages_StickerSet &result) {
|
)).done([=](const MTPmessages_StickerSet &result) {
|
||||||
|
_requestId = 0;
|
||||||
refreshDelayed();
|
refreshDelayed();
|
||||||
result.match([&](const MTPDmessages_stickerSet &data) {
|
result.match([&](const MTPDmessages_stickerSet &data) {
|
||||||
auto map = base::flat_map<uint64, not_null<DocumentData*>>();
|
applySet(data);
|
||||||
for (const auto &sticker : data.vdocuments().v) {
|
|
||||||
const auto document = _session->data().processDocument(
|
|
||||||
sticker);
|
|
||||||
if (document->sticker()) {
|
|
||||||
map.emplace(document->id, document);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto &pack : data.vpacks().v) {
|
|
||||||
pack.match([&](const MTPDstickerPack &data) {
|
|
||||||
const auto emoji = [&] {
|
|
||||||
return Ui::Emoji::Find(qs(data.vemoticon()));
|
|
||||||
}();
|
|
||||||
const auto document = [&]() -> DocumentData* {
|
|
||||||
for (const auto &id : data.vdocuments().v) {
|
|
||||||
const auto i = map.find(id.v);
|
|
||||||
if (i != end(map)) {
|
|
||||||
return i->second.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}();
|
|
||||||
if (emoji && document) {
|
|
||||||
_map.emplace_or_assign(emoji, document);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
int a = 0;
|
|
||||||
}).fail([=](const RPCError &error) {
|
}).fail([=](const RPCError &error) {
|
||||||
|
_requestId = 0;
|
||||||
refreshDelayed();
|
refreshDelayed();
|
||||||
}).send();
|
}).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EmojiPack::applySet(const MTPDmessages_stickerSet &data) {
|
||||||
|
const auto stickers = collectStickers(data.vdocuments().v);
|
||||||
|
auto was = base::take(_map);
|
||||||
|
|
||||||
|
for (const auto &pack : data.vpacks().v) {
|
||||||
|
pack.match([&](const MTPDstickerPack &data) {
|
||||||
|
applyPack(data, stickers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &[emoji, document] : _map) {
|
||||||
|
const auto i = was.find(emoji);
|
||||||
|
if (i == end(was)) {
|
||||||
|
refreshItems(emoji);
|
||||||
|
} else {
|
||||||
|
if (i->second != document) {
|
||||||
|
refreshItems(i->first);
|
||||||
|
}
|
||||||
|
was.erase(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto &[emoji, Document] : was) {
|
||||||
|
refreshItems(emoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPack::refreshItems(EmojiPtr emoji) {
|
||||||
|
const auto i = _items.find(emoji);
|
||||||
|
if (i == end(_items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto &item : i->second) {
|
||||||
|
_session->data().requestItemViewRefresh(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPack::applyPack(
|
||||||
|
const MTPDstickerPack &data,
|
||||||
|
const base::flat_map<uint64, not_null<DocumentData*>> &map) {
|
||||||
|
const auto emoji = [&] {
|
||||||
|
return Ui::Emoji::Find(qs(data.vemoticon()));
|
||||||
|
}();
|
||||||
|
const auto document = [&]() -> DocumentData * {
|
||||||
|
for (const auto &id : data.vdocuments().v) {
|
||||||
|
const auto i = map.find(id.v);
|
||||||
|
if (i != end(map)) {
|
||||||
|
return i->second.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}();
|
||||||
|
if (emoji && document) {
|
||||||
|
_map.emplace_or_assign(emoji, document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base::flat_map<uint64, not_null<DocumentData*>> EmojiPack::collectStickers(
|
||||||
|
const QVector<MTPDocument> &list) const {
|
||||||
|
auto result = base::flat_map<uint64, not_null<DocumentData*>>();
|
||||||
|
for (const auto &sticker : list) {
|
||||||
|
const auto document = _session->data().processDocument(
|
||||||
|
sticker);
|
||||||
|
if (document->sticker()) {
|
||||||
|
result.emplace(document->id, document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void EmojiPack::refreshDelayed() {
|
void EmojiPack::refreshDelayed() {
|
||||||
App::CallDelayed(kRefreshTimeout, _session, [=] {
|
App::CallDelayed(kRefreshTimeout, _session, [=] {
|
||||||
refresh();
|
refresh();
|
||||||
|
|
|
@ -20,15 +20,26 @@ class EmojiPack final {
|
||||||
public:
|
public:
|
||||||
explicit EmojiPack(not_null<Main::Session*> session);
|
explicit EmojiPack(not_null<Main::Session*> session);
|
||||||
|
|
||||||
|
bool add(not_null<HistoryItem*> item, const QString &text);
|
||||||
|
bool remove(not_null<const HistoryItem*> item);
|
||||||
|
|
||||||
[[nodiscard]] DocumentData *stickerForEmoji(not_null<HistoryItem*> item);
|
[[nodiscard]] DocumentData *stickerForEmoji(not_null<HistoryItem*> item);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refresh();
|
void refresh();
|
||||||
void refreshDelayed();
|
void refreshDelayed();
|
||||||
|
void applySet(const MTPDmessages_stickerSet &data);
|
||||||
|
void applyPack(
|
||||||
|
const MTPDstickerPack &data,
|
||||||
|
const base::flat_map<uint64, not_null<DocumentData*>> &map);
|
||||||
|
base::flat_map<uint64, not_null<DocumentData*>> collectStickers(
|
||||||
|
const QVector<MTPDocument> &list) const;
|
||||||
|
void refreshItems(EmojiPtr emoji);
|
||||||
|
|
||||||
not_null<Main::Session*> _session;
|
not_null<Main::Session*> _session;
|
||||||
base::flat_set<not_null<HistoryItem*>> _notLoaded;
|
base::flat_set<not_null<HistoryItem*>> _notLoaded;
|
||||||
base::flat_map<EmojiPtr, not_null<DocumentData*>> _map;
|
base::flat_map<EmojiPtr, not_null<DocumentData*>> _map;
|
||||||
|
base::flat_map<EmojiPtr, base::flat_set<not_null<HistoryItem*>>> _items;
|
||||||
mtpRequestId _requestId = 0;
|
mtpRequestId _requestId = 0;
|
||||||
|
|
||||||
rpl::lifetime _lifetime;
|
rpl::lifetime _lifetime;
|
||||||
|
|
|
@ -1680,7 +1680,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||||
const auto media = (view ? view->media() : nullptr);
|
const auto media = (view ? view->media() : nullptr);
|
||||||
const auto mediaHasTextForCopy = media && media->hasTextForCopy();
|
const auto mediaHasTextForCopy = media && media->hasTextForCopy();
|
||||||
if (const auto document = media ? media->getDocument() : nullptr) {
|
if (const auto document = media ? media->getDocument() : nullptr) {
|
||||||
if (document->sticker()) {
|
if (!item->isSingleEmoji() && document->sticker()) {
|
||||||
if (document->sticker()->set.type() != mtpc_inputStickerSetEmpty) {
|
if (document->sticker()->set.type() != mtpc_inputStickerSetEmpty) {
|
||||||
_menu->addAction(document->isStickerSetInstalled() ? tr::lng_context_pack_info(tr::now) : tr::lng_context_pack_add(tr::now), [=] {
|
_menu->addAction(document->isStickerSetInstalled() ? tr::lng_context_pack_info(tr::now) : tr::lng_context_pack_add(tr::now), [=] {
|
||||||
showStickerPackInfo(document);
|
showStickerPackInfo(document);
|
||||||
|
|
|
@ -139,19 +139,19 @@ public:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool definesReplyKeyboard() const;
|
[[nodiscard]] bool definesReplyKeyboard() const;
|
||||||
MTPDreplyKeyboardMarkup::Flags replyKeyboardFlags() const;
|
[[nodiscard]] MTPDreplyKeyboardMarkup::Flags replyKeyboardFlags() const;
|
||||||
|
|
||||||
bool hasSwitchInlineButton() const {
|
[[nodiscard]] bool hasSwitchInlineButton() const {
|
||||||
return _flags & MTPDmessage_ClientFlag::f_has_switch_inline_button;
|
return _flags & MTPDmessage_ClientFlag::f_has_switch_inline_button;
|
||||||
}
|
}
|
||||||
bool hasTextLinks() const {
|
[[nodiscard]] bool hasTextLinks() const {
|
||||||
return _flags & MTPDmessage_ClientFlag::f_has_text_links;
|
return _flags & MTPDmessage_ClientFlag::f_has_text_links;
|
||||||
}
|
}
|
||||||
bool isGroupEssential() const {
|
[[nodiscard]] bool isGroupEssential() const {
|
||||||
return _flags & MTPDmessage_ClientFlag::f_is_group_essential;
|
return _flags & MTPDmessage_ClientFlag::f_is_group_essential;
|
||||||
}
|
}
|
||||||
bool isLocalUpdateMedia() const {
|
[[nodiscard]] bool isLocalUpdateMedia() const {
|
||||||
return _flags & MTPDmessage_ClientFlag::f_is_local_update_media;
|
return _flags & MTPDmessage_ClientFlag::f_is_local_update_media;
|
||||||
}
|
}
|
||||||
void setIsLocalUpdateMedia(bool flag) {
|
void setIsLocalUpdateMedia(bool flag) {
|
||||||
|
@ -161,22 +161,25 @@ public:
|
||||||
_flags &= ~MTPDmessage_ClientFlag::f_is_local_update_media;
|
_flags &= ~MTPDmessage_ClientFlag::f_is_local_update_media;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bool isGroupMigrate() const {
|
[[nodiscard]] bool isGroupMigrate() const {
|
||||||
return isGroupEssential() && isEmpty();
|
return isGroupEssential() && isEmpty();
|
||||||
}
|
}
|
||||||
bool hasViews() const {
|
[[nodiscard]] bool isSingleEmoji() const {
|
||||||
|
return _flags & MTPDmessage_ClientFlag::f_single_emoji;
|
||||||
|
}
|
||||||
|
[[nodiscard]] bool hasViews() const {
|
||||||
return _flags & MTPDmessage::Flag::f_views;
|
return _flags & MTPDmessage::Flag::f_views;
|
||||||
}
|
}
|
||||||
bool isPost() const {
|
[[nodiscard]] bool isPost() const {
|
||||||
return _flags & MTPDmessage::Flag::f_post;
|
return _flags & MTPDmessage::Flag::f_post;
|
||||||
}
|
}
|
||||||
bool isSilent() const {
|
[[nodiscard]] bool isSilent() const {
|
||||||
return _flags & MTPDmessage::Flag::f_silent;
|
return _flags & MTPDmessage::Flag::f_silent;
|
||||||
}
|
}
|
||||||
bool isSending() const {
|
[[nodiscard]] bool isSending() const {
|
||||||
return _flags & MTPDmessage_ClientFlag::f_sending;
|
return _flags & MTPDmessage_ClientFlag::f_sending;
|
||||||
}
|
}
|
||||||
bool hasFailed() const {
|
[[nodiscard]] bool hasFailed() const {
|
||||||
return _flags & MTPDmessage_ClientFlag::f_failed;
|
return _flags & MTPDmessage_ClientFlag::f_failed;
|
||||||
}
|
}
|
||||||
void sendFailed();
|
void sendFailed();
|
||||||
|
@ -224,6 +227,9 @@ public:
|
||||||
virtual QString inReplyText() const {
|
virtual QString inReplyText() const {
|
||||||
return inDialogsText(DrawInDialog::WithoutSender);
|
return inDialogsText(DrawInDialog::WithoutSender);
|
||||||
}
|
}
|
||||||
|
virtual QString originalString() const {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
virtual TextWithEntities originalText() const {
|
virtual TextWithEntities originalText() const {
|
||||||
return TextWithEntities();
|
return TextWithEntities();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "history/history_service.h"
|
#include "history/history_service.h"
|
||||||
#include "history/view/history_view_service_message.h"
|
#include "history/view/history_view_service_message.h"
|
||||||
#include "history/view/history_view_context_menu.h" // For CopyPostLink().
|
#include "history/view/history_view_context_menu.h" // For CopyPostLink().
|
||||||
|
#include "chat_helpers/stickers_emoji_pack.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "boxes/share_box.h"
|
#include "boxes/share_box.h"
|
||||||
#include "boxes/confirm_box.h"
|
#include "boxes/confirm_box.h"
|
||||||
|
@ -1083,6 +1084,7 @@ void HistoryMessage::setText(const TextWithEntities &textWithEntities) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearSingleEmoji();
|
||||||
if (_media && _media->consumeMessageText(textWithEntities)) {
|
if (_media && _media->consumeMessageText(textWithEntities)) {
|
||||||
setEmptyText();
|
setEmptyText();
|
||||||
} else {
|
} else {
|
||||||
|
@ -1095,8 +1097,10 @@ void HistoryMessage::setText(const TextWithEntities &textWithEntities) {
|
||||||
// just replace it with something so that UI won't look buggy.
|
// just replace it with something so that UI won't look buggy.
|
||||||
_text.setMarkedText(
|
_text.setMarkedText(
|
||||||
st::messageTextStyle,
|
st::messageTextStyle,
|
||||||
{ QString::fromUtf8("\xF0\x9F\x98\x94"), EntitiesInText() },
|
{ QString::fromUtf8(":-("), EntitiesInText() },
|
||||||
Ui::ItemTextOptions(this));
|
Ui::ItemTextOptions(this));
|
||||||
|
} else if (!_media) {
|
||||||
|
checkSingleEmoji(textWithEntities.text);
|
||||||
}
|
}
|
||||||
_textWidth = -1;
|
_textWidth = -1;
|
||||||
_textHeight = 0;
|
_textHeight = 0;
|
||||||
|
@ -1113,6 +1117,20 @@ void HistoryMessage::setEmptyText() {
|
||||||
_textHeight = 0;
|
_textHeight = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HistoryMessage::clearSingleEmoji() {
|
||||||
|
if (!(_flags & MTPDmessage_ClientFlag::f_single_emoji)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
history()->session().emojiStickersPack().remove(this);
|
||||||
|
_flags &= ~MTPDmessage_ClientFlag::f_single_emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HistoryMessage::checkSingleEmoji(const QString &text) {
|
||||||
|
if (history()->session().emojiStickersPack().add(this, text)) {
|
||||||
|
_flags |= MTPDmessage_ClientFlag::f_single_emoji;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void HistoryMessage::setReplyMarkup(const MTPReplyMarkup *markup) {
|
void HistoryMessage::setReplyMarkup(const MTPReplyMarkup *markup) {
|
||||||
if (!markup) {
|
if (!markup) {
|
||||||
if (_flags & MTPDmessage::Flag::f_reply_markup) {
|
if (_flags & MTPDmessage::Flag::f_reply_markup) {
|
||||||
|
@ -1155,6 +1173,10 @@ void HistoryMessage::setReplyMarkup(const MTPReplyMarkup *markup) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString HistoryMessage::originalString() const {
|
||||||
|
return emptyText() ? QString() : _text.toString();
|
||||||
|
}
|
||||||
|
|
||||||
TextWithEntities HistoryMessage::originalText() const {
|
TextWithEntities HistoryMessage::originalText() const {
|
||||||
if (emptyText()) {
|
if (emptyText()) {
|
||||||
return { QString(), EntitiesInText() };
|
return { QString(), EntitiesInText() };
|
||||||
|
|
|
@ -29,8 +29,7 @@ QString GetErrorTextForForward(
|
||||||
bool ignoreSlowmodeCountdown = false);
|
bool ignoreSlowmodeCountdown = false);
|
||||||
void FastShareMessage(not_null<HistoryItem*> item);
|
void FastShareMessage(not_null<HistoryItem*> item);
|
||||||
|
|
||||||
class HistoryMessage
|
class HistoryMessage : public HistoryItem {
|
||||||
: public HistoryItem {
|
|
||||||
public:
|
public:
|
||||||
HistoryMessage(
|
HistoryMessage(
|
||||||
not_null<History*> history,
|
not_null<History*> history,
|
||||||
|
@ -134,6 +133,7 @@ public:
|
||||||
[[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const override;
|
[[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const override;
|
||||||
|
|
||||||
void setText(const TextWithEntities &textWithEntities) override;
|
void setText(const TextWithEntities &textWithEntities) override;
|
||||||
|
[[nodiscard]] QString originalString() const override;
|
||||||
[[nodiscard]] TextWithEntities originalText() const override;
|
[[nodiscard]] TextWithEntities originalText() const override;
|
||||||
[[nodiscard]] TextForMimeData clipboardText() const override;
|
[[nodiscard]] TextForMimeData clipboardText() const override;
|
||||||
[[nodiscard]] bool textHasLinks() const override;
|
[[nodiscard]] bool textHasLinks() const override;
|
||||||
|
@ -164,6 +164,9 @@ private:
|
||||||
return _flags & MTPDmessage::Flag::f_legacy;
|
return _flags & MTPDmessage::Flag::f_legacy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearSingleEmoji();
|
||||||
|
void checkSingleEmoji(const QString &text);
|
||||||
|
|
||||||
// For an invoice button we replace the button text with a "Receipt" key.
|
// For an invoice button we replace the button text with a "Receipt" key.
|
||||||
// It should show the receipt for the payed invoice. Still let mobile apps do that.
|
// It should show the receipt for the payed invoice. Still let mobile apps do that.
|
||||||
void replaceBuyWithReceiptInMarkup();
|
void replaceBuyWithReceiptInMarkup();
|
||||||
|
|
|
@ -63,8 +63,11 @@ enum class MTPDmessage_ClientFlag : uint32 {
|
||||||
// message was an outgoing message and failed to be sent
|
// message was an outgoing message and failed to be sent
|
||||||
f_failed = (1U << 22),
|
f_failed = (1U << 22),
|
||||||
|
|
||||||
|
// message has no media and only a single emoji text
|
||||||
|
f_single_emoji = (1U << 21),
|
||||||
|
|
||||||
// update this when adding new client side flags
|
// update this when adding new client side flags
|
||||||
MIN_FIELD = (1U << 22),
|
MIN_FIELD = (1U << 21),
|
||||||
};
|
};
|
||||||
DEFINE_MTP_CLIENT_FLAGS(MTPDmessage)
|
DEFINE_MTP_CLIENT_FLAGS(MTPDmessage)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue