/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "chat_helpers/emoji_interactions.h" #include "chat_helpers/stickers_emoji_pack.h" #include "history/history_item.h" #include "history/history.h" #include "history/view/history_view_element.h" #include "history/view/media/history_view_sticker.h" #include "main/main_session.h" #include "data/data_changes.h" #include "data/data_peer.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "ui/emoji_config.h" #include "base/random.h" #include "apiwrap.h" #include <QtCore/QJsonDocument> #include <QtCore/QJsonArray> #include <QtCore/QJsonObject> #include <QtCore/QJsonValue> namespace ChatHelpers { namespace { constexpr auto kMinDelay = crl::time(200); constexpr auto kAccumulateDelay = crl::time(1000); constexpr auto kMaxDelay = 2 * crl::time(1000); constexpr auto kTimeNever = std::numeric_limits<crl::time>::max(); constexpr auto kVersion = 1; } // namespace auto EmojiInteractions::Combine(CheckResult a, CheckResult b) -> CheckResult { return { .nextCheckAt = std::min(a.nextCheckAt, b.nextCheckAt), .waitingForDownload = a.waitingForDownload || b.waitingForDownload, }; } EmojiInteractions::EmojiInteractions(not_null<Main::Session*> session) : _session(session) , _checkTimer([=] { check(); }) { _session->changes().messageUpdates( Data::MessageUpdate::Flag::Destroyed ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { _animations.remove(update.item); }, _lifetime); } EmojiInteractions::~EmojiInteractions() = default; void EmojiInteractions::start(not_null<const HistoryView::Element*> view) { const auto item = view->data(); if (!IsServerMsgId(item->id) || !item->history()->peer->isUser()) { return; } const auto emoji = Ui::Emoji::Find(item->originalText().text); if (!emoji) { return; } const auto &pack = _session->emojiStickersPack(); const auto &list = pack.animationsForEmoji(emoji); if (list.empty()) { return; } auto &animations = _animations[item]; if (!animations.empty() && animations.front().emoji != emoji) { // The message was edited, forget the old emoji. animations.clear(); } const auto last = !animations.empty() ? &animations.back() : nullptr; const auto listSize = int(list.size()); const auto chooseDifferent = (last && listSize > 1); const auto index = chooseDifferent ? base::RandomIndex(listSize - 1) : base::RandomIndex(listSize); const auto selected = (begin(list) + index)->second; const auto document = (chooseDifferent && selected == last->document) ? (begin(list) + index + 1)->second : selected; const auto media = document->createMediaView(); media->checkStickerLarge(); const auto now = crl::now(); animations.push_back({ .emoji = emoji, .document = document, .media = media, .scheduledAt = now, .index = index, }); check(now); } auto EmojiInteractions::checkAnimations(crl::time now) -> CheckResult { auto nearest = kTimeNever; auto waitingForDownload = false; for (auto &[item, animations] : _animations) { auto lastStartedAt = crl::time(); // Erase too old requests. const auto i = ranges::find_if(animations, [&](const Animation &a) { return !a.startedAt && (a.scheduledAt + kMaxDelay <= now); }); if (i != end(animations)) { animations.erase(i, end(animations)); } for (auto &animation : animations) { if (animation.startedAt) { lastStartedAt = animation.startedAt; } else if (!animation.media->loaded()) { animation.media->checkStickerLarge(); waitingForDownload = true; break; } else if (!lastStartedAt || lastStartedAt + kMinDelay <= now) { animation.startedAt = now; _playRequests.fire({ item, animation.media, animation.scheduledAt, }); break; } else { nearest = std::min(nearest, lastStartedAt + kMinDelay); break; } } } return { .nextCheckAt = nearest, .waitingForDownload = waitingForDownload, }; } void EmojiInteractions::sendAccumulated( crl::time now, not_null<HistoryItem*> item, std::vector<Animation> &animations) { Expects(!animations.empty()); const auto firstStartedAt = animations.front().startedAt; const auto intervalEnd = firstStartedAt + kAccumulateDelay; if (intervalEnd > now) { return; } const auto from = begin(animations); const auto till = ranges::find_if(animations, [&](const auto &animation) { return !animation.startedAt || (animation.startedAt >= intervalEnd); }); auto list = QJsonArray(); for (const auto &animation : ranges::make_subrange(from, till)) { list.push_back(QJsonObject{ { "i", (animation.index + 1) }, { "t", (animation.startedAt - firstStartedAt) / 1000. }, }); } if (list.empty()) { return; } const auto json = QJsonDocument(QJsonObject{ { "v", kVersion }, { "a", std::move(list) }, }).toJson(QJsonDocument::Compact); _session->api().request(MTPmessages_SetTyping( MTP_flags(0), item->history()->peer->input, MTPint(), // top_msg_id MTP_sendMessageEmojiInteraction( MTP_string(from->emoji->text()), MTP_int(item->id), MTP_dataJSON(MTP_bytes(json))) )).send(); animations.erase(from, till); } auto EmojiInteractions::checkAccumulated(crl::time now) -> CheckResult { auto nearest = kTimeNever; for (auto i = begin(_animations); i != end(_animations);) { auto &[item, animations] = *i; sendAccumulated(now, item, animations); if (animations.empty()) { i = _animations.erase(i); continue; } else if (const auto firstStartedAt = animations.front().startedAt) { nearest = std::min(nearest, firstStartedAt + kAccumulateDelay); Assert(nearest > now); } ++i; } return { .nextCheckAt = nearest, }; } void EmojiInteractions::check(crl::time now) { if (!now) { now = crl::now(); } const auto result1 = checkAnimations(now); const auto result2 = checkAccumulated(now); const auto result = Combine(result1, result2); if (result.nextCheckAt < kTimeNever) { Assert(result.nextCheckAt > now); _checkTimer.callOnce(result.nextCheckAt - now); } setWaitingForDownload(result.waitingForDownload); } void EmojiInteractions::setWaitingForDownload(bool waiting) { if (_waitingForDownload == waiting) { return; } _waitingForDownload = waiting; if (_waitingForDownload) { _session->downloaderTaskFinished( ) | rpl::start_with_next([=] { check(); }, _downloadCheckLifetime); } else { _downloadCheckLifetime.destroy(); _downloadCheckLifetime.destroy(); } } } // namespace ChatHelpers