diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 11f7c21f75..9eb5bec587 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -285,6 +285,8 @@ PRIVATE chat_helpers/bot_command.h chat_helpers/bot_keyboard.cpp chat_helpers/bot_keyboard.h + chat_helpers/emoji_interactions.cpp + chat_helpers/emoji_interactions.h chat_helpers/emoji_keywords.cpp chat_helpers/emoji_keywords.h chat_helpers/emoji_list_widget.cpp diff --git a/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp b/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp new file mode 100644 index 0000000000..4d20b79ea8 --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp @@ -0,0 +1,223 @@ +/* +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 +#include +#include +#include + +namespace ChatHelpers { +namespace { + +constexpr auto kMinDelay = crl::time(200); +constexpr auto kAccumulateDelay = crl::time(1000); +constexpr auto kTimeNever = std::numeric_limits::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 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 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 &[id, animations] : _animations) { + auto lastStartedAt = crl::time(); + 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; + + // #TODO interactions + //const auto sticker = std::make_unique( + // view, + // document); + break; + } else { + nearest = std::min(nearest, lastStartedAt + kMinDelay); + break; + } + } + } + return { + .nextCheckAt = nearest, + .waitingForDownload = waitingForDownload, + }; +} + +void EmojiInteractions::sendAccumulated( + crl::time now, + not_null item, + std::vector &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 &[id, animations] = *i; + sendAccumulated(now, id, 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 diff --git a/Telegram/SourceFiles/chat_helpers/emoji_interactions.h b/Telegram/SourceFiles/chat_helpers/emoji_interactions.h new file mode 100644 index 0000000000..2e06eff414 --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/emoji_interactions.h @@ -0,0 +1,74 @@ +/* +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 +*/ +#pragma once + +#include "base/timer.h" + +class HistoryItem; +class DocumentData; + +namespace Data { +class DocumentMedia; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace HistoryView { +class Element; +} // namespace HistoryView + +namespace ChatHelpers { + +class EmojiInteractions final { +public: + explicit EmojiInteractions(not_null session); + ~EmojiInteractions(); + + void start(not_null view); + +private: + struct Animation { + EmojiPtr emoji; + not_null document; + std::shared_ptr media; + crl::time scheduledAt = 0; + crl::time startedAt = 0; + int index = 0; + }; + struct CheckResult { + crl::time nextCheckAt = 0; + bool waitingForDownload = false; + }; + [[nodiscard]] static CheckResult Combine(CheckResult a, CheckResult b); + + void check(crl::time now = 0); + [[nodiscard]] CheckResult checkAnimations(crl::time now); + [[nodiscard]] CheckResult checkAccumulated(crl::time now); + void sendAccumulated( + crl::time now, + not_null item, + std::vector &animations); + void setWaitingForDownload(bool waiting); + + const not_null _session; + + base::flat_map< + not_null, + std::vector> _animations; + base::Timer _checkTimer; + + bool _waitingForDownload = false; + rpl::lifetime _downloadCheckLifetime; + + rpl::lifetime _lifetime; + +}; + +} // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp index c0b4cdac61..b00db95ba5 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp @@ -32,14 +32,14 @@ constexpr auto kRefreshTimeout = 7200 * crl::time(1000); [[nodiscard]] std::optional IndexFromEmoticon(const QString &emoticon) { if (emoticon.size() < 2) { - return -1; + return std::nullopt; } const auto first = emoticon[0].unicode(); return (first >= '1' && first <= '9') - ? (first - '1') + ? std::make_optional(first - '1') : (first == 55357 && emoticon[1].unicode() == 56607) - ? 9 - : -1; + ? std::make_optional(9) + : std::nullopt; } [[nodiscard]] QSize SingleSize() { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index eaa84b00f8..17d3b5af5a 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/history_inner_widget.h" -#include #include "core/file_utilities.h" #include "core/crash_reports.h" #include "core/click_handler_types.h" @@ -44,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/confirm_box.h" #include "boxes/sticker_set_box.h" #include "chat_helpers/message_field.h" +#include "chat_helpers/emoji_interactions.h" #include "history/history_widget.h" #include "base/platform/base_platform_info.h" #include "base/unixtime.h" @@ -2690,6 +2690,7 @@ void HistoryInner::elementReplyTo(const FullMsgId &to) { } void HistoryInner::elementStartInteraction(not_null view) { + _controller->emojiInteractions().start(view); } auto HistoryInner::getSelectionState() const diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp index ad9672df07..0bdc3dfe87 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp @@ -295,14 +295,9 @@ void Sticker::refreshLink() { if (isEmojiSticker()) { const auto weak = base::make_weak(this); _link = std::make_shared([weak] { - const auto that = weak.get(); - if (!that || !that->_lottieOncePlayed) { - return; + if (const auto that = weak.get()) { + that->emojiStickerClicked(); } - that->_parent->delegate()->elementStartInteraction(that->_parent); - that->_lottieOncePlayed = false; - that->_parent->history()->owner().requestViewRepaint( - that->_parent); }); } else if (sticker && sticker->set) { _link = std::make_shared([document = _data](ClickContext context) { @@ -328,6 +323,14 @@ void Sticker::refreshLink() { } } +void Sticker::emojiStickerClicked() { + if (_lottie) { + _parent->delegate()->elementStartInteraction(_parent); + } + _lottieOncePlayed = false; + _parent->history()->owner().requestViewRepaint(_parent); +} + void Sticker::ensureDataMediaCreated() const { if (_dataMedia) { return; diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.h b/Telegram/SourceFiles/history/view/media/history_view_sticker.h index 2422e594e4..8b496e83bc 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.h +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.h @@ -97,6 +97,7 @@ private: void setupLottie(); void lottieCreated(); void unloadLottie(); + void emojiStickerClicked(); const not_null _parent; const not_null _data; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index b4ab89537d..1a3c944580 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "passport/passport_form_controller.h" #include "chat_helpers/tabbed_selector.h" +#include "chat_helpers/emoji_interactions.h" #include "core/shortcuts.h" #include "core/application.h" #include "core/core_settings.h" @@ -518,6 +519,8 @@ SessionController::SessionController( not_null window) : SessionNavigation(session) , _window(window) +, _emojiInteractions( + std::make_unique(session)) , _tabbedSelector( std::make_unique( _window->widget(), diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 6904dbded6..35c79a856b 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -27,6 +27,7 @@ enum class WindowLayout; namespace ChatHelpers { class TabbedSelector; +class EmojiInteractions; } // namespace ChatHelpers namespace Main { @@ -247,6 +248,9 @@ public: [[nodiscard]] not_null<::MainWindow*> widget() const; [[nodiscard]] not_null content() const; [[nodiscard]] Adaptive &adaptive() const; + [[nodiscard]] ChatHelpers::EmojiInteractions &emojiInteractions() const { + return *_emojiInteractions; + } // We need access to this from MainWidget::MainWidget, where // we can't call content() yet. @@ -462,6 +466,7 @@ private: bool generateGradient = true) const; const not_null _window; + const std::unique_ptr _emojiInteractions; std::unique_ptr _passportForm; std::unique_ptr _filters;