diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index dfc8cea8b..a4a07a075 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_scheduled_messages.h" #include "data/data_send_action.h" +#include "chat_helpers/emoji_interactions.h" #include "lang/lang_cloud_manager.h" #include "history/history.h" #include "history/history_item.h" @@ -984,36 +985,24 @@ void Updates::handleSendActionUpdate( const auto from = (fromId == session().userPeerId()) ? session().user().get() : session().data().peerLoaded(fromId); - const auto isSpeakingInCall = (action.type() - == mtpc_speakingInGroupCallAction); - if (isSpeakingInCall) { - if (!peer->isChat() && !peer->isChannel()) { - return; - } - const auto call = peer->groupCall(); - const auto now = crl::now(); - if (call) { - call->applyActiveUpdate( - fromId, - Data::LastSpokeTimes{ .anything = now, .voice = now }, - from); - } else { - const auto chat = peer->asChat(); - const auto channel = peer->asChannel(); - const auto active = chat - ? (chat->flags() & ChatDataFlag::CallActive) - : (channel->flags() & ChannelDataFlag::CallActive); - if (active) { - _pendingSpeakingCallParticipants.emplace( - peer).first->second[fromId] = now; - if (peerIsUser(fromId)) { - session().api().requestFullPeer(peer); - } - } - } + if (action.type() == mtpc_speakingInGroupCallAction) { + handleSpeakingInCall(peer, fromId, from); } if (!from || !from->isUser() || from->isSelf()) { return; + } else if (action.type() == mtpc_sendMessageEmojiInteraction) { + const auto &data = action.c_sendMessageEmojiInteraction(); + const auto json = data.vinteraction().match([&]( + const MTPDdataJSON &data) { + return data.vdata().v; + }); + const auto emoticon = qs(data.vemoticon()); + handleEmojiInteraction( + peer, + data.vmsg_id().v, + qs(data.vemoticon()), + ChatHelpers::EmojiInteractions::Parse(json)); + return; } const auto when = requestingDifference() ? 0 @@ -1026,6 +1015,52 @@ void Updates::handleSendActionUpdate( when); } +void Updates::handleSpeakingInCall( + not_null peer, + PeerId participantPeerId, + PeerData *participantPeerLoaded) { + if (!peer->isChat() && !peer->isChannel()) { + return; + } + const auto call = peer->groupCall(); + const auto now = crl::now(); + if (call) { + call->applyActiveUpdate( + participantPeerId, + Data::LastSpokeTimes{ .anything = now, .voice = now }, + participantPeerLoaded); + } else { + const auto chat = peer->asChat(); + const auto channel = peer->asChannel(); + const auto active = chat + ? (chat->flags() & ChatDataFlag::CallActive) + : (channel->flags() & ChannelDataFlag::CallActive); + if (active) { + _pendingSpeakingCallParticipants.emplace( + peer).first->second[participantPeerId] = now; + if (peerIsUser(participantPeerId)) { + session().api().requestFullPeer(peer); + } + } + } +} + +void Updates::handleEmojiInteraction( + not_null peer, + MsgId messageId, + const QString &emoticon, + ChatHelpers::EmojiInteractionsBunch bunch) { + if (session().windows().empty()) { + return; + } + const auto window = session().windows().front(); + window->emojiInteractions().startIncoming( + peer, + messageId, + emoticon, + std::move(bunch)); +} + void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { switch (updates.type()) { case mtpc_updateShortMessage: { diff --git a/Telegram/SourceFiles/api/api_updates.h b/Telegram/SourceFiles/api/api_updates.h index 91635592e..b3781544e 100644 --- a/Telegram/SourceFiles/api/api_updates.h +++ b/Telegram/SourceFiles/api/api_updates.h @@ -21,6 +21,10 @@ namespace Main { class Session; } // namespace Main +namespace ChatHelpers { +struct EmojiInteractionsBunch; +} // namespace ChatHelpers + namespace Api { class Updates final { @@ -139,6 +143,15 @@ private: MsgId rootId, PeerId fromId, const MTPSendMessageAction &action); + void handleSpeakingInCall( + not_null peer, + PeerId participantPeerId, + PeerData *participantPeerLoaded); + void handleEmojiInteraction( + not_null peer, + MsgId messageId, + const QString &emoticon, + ChatHelpers::EmojiInteractionsBunch bunch); const not_null _session; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp b/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp index 3a27c2b31..c968b7433 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" #include "history/view/media/history_view_sticker.h" #include "main/main_session.h" +#include "data/data_session.h" #include "data/data_changes.h" #include "data/data_peer.h" #include "data/data_document.h" @@ -33,7 +34,7 @@ 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::max(); -constexpr auto kVersion = 1; +constexpr auto kJsonVersion = 1; } // namespace @@ -49,14 +50,32 @@ EmojiInteractions::EmojiInteractions(not_null session) , _checkTimer([=] { check(); }) { _session->changes().messageUpdates( Data::MessageUpdate::Flag::Destroyed + | Data::MessageUpdate::Flag::Edited ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { - _animations.remove(update.item); + if (update.flags & Data::MessageUpdate::Flag::Destroyed) { + _outgoing.remove(update.item); + _incoming.remove(update.item); + } else if (update.flags & Data::MessageUpdate::Flag::Edited) { + checkEdition(update.item, _outgoing); + checkEdition(update.item, _incoming); + } }, _lifetime); } EmojiInteractions::~EmojiInteractions() = default; -void EmojiInteractions::start(not_null view) { +void EmojiInteractions::checkEdition( + not_null item, + base::flat_map, std::vector> &map) { + const auto i = map.find(item); + if (i != end(map) + && (i->second.front().emoji + != Ui::Emoji::Find(item->originalText().text))) { + map.erase(i); + } +} + +void EmojiInteractions::startOutgoing(not_null view) { const auto item = view->data(); if (!IsServerMsgId(item->id) || !item->history()->peer->isUser()) { return; @@ -70,7 +89,7 @@ void EmojiInteractions::start(not_null view) { if (list.empty()) { return; } - auto &animations = _animations[item]; + auto &animations = _outgoing[item]; if (!animations.empty() && animations.front().emoji != emoji) { // The message was edited, forget the old emoji. animations.clear(); @@ -98,10 +117,76 @@ void EmojiInteractions::start(not_null view) { check(now); } +void EmojiInteractions::startIncoming( + not_null peer, + MsgId messageId, + const QString &emoticon, + EmojiInteractionsBunch &&bunch) { + if (!peer->isUser() + || bunch.interactions.empty() + || !IsServerMsgId(messageId)) { + return; + } + const auto item = _session->data().message(nullptr, messageId); + if (!item) { + return; + } + const auto emoji = Ui::Emoji::Find(item->originalText().text); + if (!emoji || emoji != Ui::Emoji::Find(emoticon)) { + return; + } + const auto &pack = _session->emojiStickersPack(); + const auto &list = pack.animationsForEmoji(emoji); + if (list.empty()) { + return; + } + auto &animations = _incoming[item]; + if (!animations.empty() && animations.front().emoji != emoji) { + // The message was edited, forget the old emoji. + animations.clear(); + } + const auto now = crl::now(); + for (const auto &single : bunch.interactions) { + const auto at = now + crl::time(std::round(single.time * 1000)); + if (!animations.empty() && animations.back().scheduledAt >= at) { + continue; + } + const auto last = !animations.empty() ? &animations.back() : nullptr; + const auto listSize = int(list.size()); + const auto index = (single.index - 1); + if (index < listSize) { + const auto document = (begin(list) + index)->second; + const auto media = document->createMediaView(); + media->checkStickerLarge(); + animations.push_back({ + .emoji = emoji, + .document = document, + .media = media, + .scheduledAt = at, + .index = index, + }); + } + } + if (animations.empty()) { + _incoming.remove(item); + } else { + check(now); + } +} + auto EmojiInteractions::checkAnimations(crl::time now) -> CheckResult { + return Combine( + checkAnimations(now, _outgoing), + checkAnimations(now, _incoming)); +} + +auto EmojiInteractions::checkAnimations( + crl::time now, + base::flat_map, std::vector> &map +) -> CheckResult { auto nearest = kTimeNever; auto waitingForDownload = false; - for (auto &[item, animations] : _animations) { + for (auto &[item, animations] : map) { auto lastStartedAt = crl::time(); // Erase too old requests. @@ -138,7 +223,7 @@ auto EmojiInteractions::checkAnimations(crl::time now) -> CheckResult { }; } -void EmojiInteractions::sendAccumulated( +void EmojiInteractions::sendAccumulatedOutgoing( crl::time now, not_null item, std::vector &animations) { @@ -153,21 +238,17 @@ void EmojiInteractions::sendAccumulated( const auto till = ranges::find_if(animations, [&](const auto &animation) { return !animation.startedAt || (animation.startedAt >= intervalEnd); }); - auto list = QJsonArray(); + auto bunch = EmojiInteractionsBunch(); + bunch.interactions.reserve(till - from); for (const auto &animation : ranges::make_subrange(from, till)) { - list.push_back(QJsonObject{ - { "i", (animation.index + 1) }, - { "t", (animation.startedAt - firstStartedAt) / 1000. }, + bunch.interactions.push_back({ + .index = animation.index + 1, + .time = (animation.startedAt - firstStartedAt) / 1000., }); } - if (list.empty()) { + if (bunch.interactions.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, @@ -175,18 +256,31 @@ void EmojiInteractions::sendAccumulated( MTP_sendMessageEmojiInteraction( MTP_string(from->emoji->text()), MTP_int(item->id), - MTP_dataJSON(MTP_bytes(json))) + MTP_dataJSON(MTP_bytes(ToJson(bunch)))) )).send(); animations.erase(from, till); } +void EmojiInteractions::clearAccumulatedIncoming( + crl::time now, + std::vector &animations) { + Expects(!animations.empty()); + + const auto from = begin(animations); + const auto till = ranges::find_if(animations, [&](const auto &animation) { + return !animation.startedAt + || (animation.startedAt + kMinDelay) > now; + }); + animations.erase(from, till); +} + auto EmojiInteractions::checkAccumulated(crl::time now) -> CheckResult { auto nearest = kTimeNever; - for (auto i = begin(_animations); i != end(_animations);) { + for (auto i = begin(_outgoing); i != end(_outgoing);) { auto &[item, animations] = *i; - sendAccumulated(now, item, animations); + sendAccumulatedOutgoing(now, item, animations); if (animations.empty()) { - i = _animations.erase(i); + i = _outgoing.erase(i); continue; } else if (const auto firstStartedAt = animations.front().startedAt) { nearest = std::min(nearest, firstStartedAt + kAccumulateDelay); @@ -194,6 +288,18 @@ auto EmojiInteractions::checkAccumulated(crl::time now) -> CheckResult { } ++i; } + for (auto i = begin(_incoming); i != end(_incoming);) { + auto &[item, animations] = *i; + clearAccumulatedIncoming(now, animations); + if (animations.empty()) { + i = _incoming.erase(i); + continue; + } else { + // Doesn't really matter when, just clear them finally. + nearest = std::min(nearest, now + kAccumulateDelay); + } + ++i; + } return { .nextCheckAt = nearest, }; @@ -229,4 +335,58 @@ void EmojiInteractions::setWaitingForDownload(bool waiting) { } } +EmojiInteractionsBunch EmojiInteractions::Parse(const QByteArray &json) { + auto error = QJsonParseError{ 0, QJsonParseError::NoError }; + const auto document = QJsonDocument::fromJson(json, &error); + if (error.error != QJsonParseError::NoError || !document.isObject()) { + LOG(("API Error: Bad interactions json received.")); + return {}; + } + const auto root = document.object(); + const auto version = root.value("v").toInt(); + if (version != kJsonVersion) { + LOG(("API Error: Bad interactions version: %1").arg(version)); + return {}; + } + const auto actions = root.value("a").toArray(); + if (actions.empty()) { + LOG(("API Error: Empty interactions list.")); + return {}; + } + auto result = EmojiInteractionsBunch(); + for (const auto &interaction : actions) { + const auto object = interaction.toObject(); + const auto index = object.value("i").toInt(); + if (index < 0 || index > 10) { + LOG(("API Error: Bad interaction index: %1").arg(index)); + return {}; + } + const auto time = object.value("t").toDouble(); + if (time < 0. + || time > 1. + || (!result.interactions.empty() + && time <= result.interactions.back().time)) { + LOG(("API Error: Bad interaction time: %1").arg(time)); + continue; + } + result.interactions.push_back({ .index = index, .time = time }); + } + + return result; +} + +QByteArray EmojiInteractions::ToJson(const EmojiInteractionsBunch &bunch) { + auto list = QJsonArray(); + for (const auto &single : bunch.interactions) { + list.push_back(QJsonObject{ + { "i", single.index }, + { "t", single.time }, + }); + } + return QJsonDocument(QJsonObject{ + { "v", kJsonVersion }, + { "a", std::move(list) }, + }).toJson(QJsonDocument::Compact); +} + } // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/emoji_interactions.h b/Telegram/SourceFiles/chat_helpers/emoji_interactions.h index d22be9979..9ae293cfd 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_interactions.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_interactions.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" +class PeerData; class HistoryItem; class DocumentData; @@ -32,6 +33,14 @@ struct EmojiInteractionPlayRequest { crl::time shouldHaveStartedAt = 0; }; +struct EmojiInteractionsBunch { + struct Single { + int index = 0; + double time = 0; + }; + std::vector interactions; +}; + class EmojiInteractions final { public: explicit EmojiInteractions(not_null session); @@ -39,11 +48,21 @@ public: using PlayRequest = EmojiInteractionPlayRequest; - void start(not_null view); + void startOutgoing(not_null view); + void startIncoming( + not_null peer, + MsgId messageId, + const QString &emoticon, + EmojiInteractionsBunch &&bunch); + [[nodiscard]] rpl::producer playRequests() const { return _playRequests.events(); } + [[nodiscard]] static EmojiInteractionsBunch Parse(const QByteArray &json); + [[nodiscard]] static QByteArray ToJson( + const EmojiInteractionsBunch &bunch); + private: struct Animation { EmojiPtr emoji; @@ -61,18 +80,27 @@ private: void check(crl::time now = 0); [[nodiscard]] CheckResult checkAnimations(crl::time now); + [[nodiscard]] CheckResult checkAnimations( + crl::time now, + base::flat_map, std::vector> &map); [[nodiscard]] CheckResult checkAccumulated(crl::time now); - void sendAccumulated( + void sendAccumulatedOutgoing( crl::time now, not_null item, std::vector &animations); + void clearAccumulatedIncoming( + crl::time now, + std::vector &animations); void setWaitingForDownload(bool waiting); + void checkEdition( + not_null item, + base::flat_map, std::vector> &map); + const not_null _session; - base::flat_map< - not_null, - std::vector> _animations; + base::flat_map, std::vector> _outgoing; + base::flat_map, std::vector> _incoming; base::Timer _checkTimer; rpl::event_stream _playRequests; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 157205df2..3fb06b247 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2720,7 +2720,7 @@ void HistoryInner::elementReplyTo(const FullMsgId &to) { } void HistoryInner::elementStartInteraction(not_null view) { - _controller->emojiInteractions().start(view); + _controller->emojiInteractions().startOutgoing(view); } auto HistoryInner::getSelectionState() const diff --git a/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp b/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp index 0d7d4103b..a2a01d101 100644 --- a/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp +++ b/Telegram/SourceFiles/history/view/history_view_emoji_interactions.cpp @@ -100,6 +100,9 @@ void EmojiInteractions::play( .lottie = std::move(lottie), .shift = shift, }); + if (const auto media = view->media()) { + media->stickerClearLoopPlayed(); + } } void EmojiInteractions::visibleAreaUpdated( diff --git a/Telegram/SourceFiles/history/view/history_view_send_action.cpp b/Telegram/SourceFiles/history/view/history_view_send_action.cpp index 42438049e..a6948a07e 100644 --- a/Telegram/SourceFiles/history/view/history_view_send_action.cpp +++ b/Telegram/SourceFiles/history/view/history_view_send_action.cpp @@ -119,7 +119,7 @@ bool SendActionPainter::updateNeedsAnimating( Type::ChooseSticker, kStatusShowClientsideChooseSticker); }, [&](const MTPDsendMessageEmojiInteraction &) { - // #TODO interaction + Unexpected("EmojiInteraction here."); }, [&](const MTPDsendMessageEmojiInteractionSeen &) { // #TODO interaction }, [&](const MTPDsendMessageCancelAction &) {