diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4ccb0fd247..537b7ed4fa 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1618,6 +1618,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_user_action_upload_file" = "{user} is sending a file"; "lng_send_action_choose_sticker" = "choosing a sticker"; "lng_user_action_choose_sticker" = "{user} is choosing a sticker"; +"lng_user_action_watching_animations" = "watching {emoji} animations"; "lng_unread_bar#one" = "{count} unread message"; "lng_unread_bar#other" = "{count} unread messages"; "lng_unread_bar_some" = "Unread messages"; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index a4a07a0756..5c2c9c8d93 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -991,17 +991,11 @@ void Updates::handleSendActionUpdate( 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)); + handleEmojiInteraction(peer, action.c_sendMessageEmojiInteraction()); + return; + } else if (action.type() == mtpc_sendMessageEmojiInteractionSeen) { + const auto &data = action.c_sendMessageEmojiInteractionSeen(); + handleEmojiInteraction(peer, qs(data.vemoticon())); return; } const auto when = requestingDifference() @@ -1015,6 +1009,20 @@ void Updates::handleSendActionUpdate( when); } +void Updates::handleEmojiInteraction( + not_null peer, + const MTPDsendMessageEmojiInteraction &data) { + const auto json = data.vinteraction().match([&]( + const MTPDdataJSON &data) { + return data.vdata().v; + }); + handleEmojiInteraction( + peer, + data.vmsg_id().v, + qs(data.vemoticon()), + ChatHelpers::EmojiInteractions::Parse(json)); +} + void Updates::handleSpeakingInCall( not_null peer, PeerId participantPeerId, @@ -1061,6 +1069,16 @@ void Updates::handleEmojiInteraction( std::move(bunch)); } +void Updates::handleEmojiInteraction( + not_null peer, + const QString &emoticon) { + if (session().windows().empty()) { + return; + } + const auto window = session().windows().front(); + window->emojiInteractions().seenOutgoing(peer, emoticon); +} + 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 b3781544ec..f21baa9641 100644 --- a/Telegram/SourceFiles/api/api_updates.h +++ b/Telegram/SourceFiles/api/api_updates.h @@ -143,6 +143,9 @@ private: MsgId rootId, PeerId fromId, const MTPSendMessageAction &action); + void handleEmojiInteraction( + not_null peer, + const MTPDsendMessageEmojiInteraction &data); void handleSpeakingInCall( not_null peer, PeerId participantPeerId, @@ -152,6 +155,9 @@ private: MsgId messageId, const QString &emoticon, ChatHelpers::EmojiInteractionsBunch bunch); + void handleEmojiInteraction( + not_null peer, + const QString &emoticon); const not_null _session; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp b/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp index e706b5315c..15d6e98d0d 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp @@ -33,6 +33,7 @@ namespace { constexpr auto kMinDelay = crl::time(200); constexpr auto kAccumulateDelay = crl::time(1000); constexpr auto kAccumulateSeenRequests = kAccumulateDelay; +constexpr auto kAcceptSeenSinceRequest = 3 * crl::time(1000); constexpr auto kMaxDelay = 2 * crl::time(1000); constexpr auto kTimeNever = std::numeric_limits::max(); constexpr auto kJsonVersion = 1; @@ -177,6 +178,21 @@ void EmojiInteractions::startIncoming( } } +void EmojiInteractions::seenOutgoing( + not_null peer, + const QString &emoticon) { + if (const auto i = _playsSent.find(peer); i != end(_playsSent)) { + if (const auto emoji = Ui::Emoji::Find(emoticon)) { + if (const auto j = i->second.find(emoji); j != end(i->second)) { + const auto last = j->second.lastDoneReceivedAt; + if (!last || last + kAcceptSeenSinceRequest > crl::now()) { + _seen.fire({ peer, emoji }); + } + } + } + } +} + auto EmojiInteractions::checkAnimations(crl::time now) -> CheckResult { return Combine( checkAnimations(now, _outgoing), @@ -254,15 +270,26 @@ void EmojiInteractions::sendAccumulatedOutgoing( if (bunch.interactions.empty()) { return; } - _session->api().request(MTPmessages_SetTyping( + const auto peer = item->history()->peer; + const auto emoji = from->emoji; + const auto requestId = _session->api().request(MTPmessages_SetTyping( MTP_flags(0), - item->history()->peer->input, + peer->input, MTPint(), // top_msg_id MTP_sendMessageEmojiInteraction( - MTP_string(from->emoji->text()), + MTP_string(emoji->text()), MTP_int(item->id), MTP_dataJSON(MTP_bytes(ToJson(bunch)))) - )).send(); + )).done([=](const MTPBool &result, mtpRequestId requestId) { + auto &sent = _playsSent[peer][emoji]; + if (sent.lastRequestId == requestId) { + sent.lastDoneReceivedAt = crl::now(); + if (!_checkTimer.isActive()) { + _checkTimer.callOnce(kAcceptSeenSinceRequest); + } + } + }).send(); + _playsSent[peer][emoji] = PlaySent{ .lastRequestId = requestId }; animations.erase(from, till); } @@ -315,6 +342,7 @@ void EmojiInteractions::check(crl::time now) { now = crl::now(); } checkSeenRequests(now); + checkSentRequests(now); const auto result1 = checkAnimations(now); const auto result2 = checkAccumulated(now); const auto result = Combine(result1, result2); @@ -323,6 +351,8 @@ void EmojiInteractions::check(crl::time now) { _checkTimer.callOnce(result.nextCheckAt - now); } else if (!_playStarted.empty()) { _checkTimer.callOnce(kAccumulateSeenRequests); + } else if (!_playsSent.empty()) { + _checkTimer.callOnce(kAcceptSeenSinceRequest); } setWaitingForDownload(result.waitingForDownload); } @@ -344,6 +374,24 @@ void EmojiInteractions::checkSeenRequests(crl::time now) { } } +void EmojiInteractions::checkSentRequests(crl::time now) { + for (auto i = begin(_playsSent); i != end(_playsSent);) { + for (auto j = begin(i->second); j != end(i->second);) { + const auto last = j->second.lastDoneReceivedAt; + if (last && last + kAcceptSeenSinceRequest <= now) { + j = i->second.erase(j); + } else { + ++j; + } + } + if (i->second.empty()) { + i = _playsSent.erase(i); + } else { + ++i; + } + } +} + void EmojiInteractions::setWaitingForDownload(bool waiting) { if (_waitingForDownload == waiting) { return; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_interactions.h b/Telegram/SourceFiles/chat_helpers/emoji_interactions.h index 60fcf3b1c0..97aaa8bae3 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_interactions.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_interactions.h @@ -43,6 +43,11 @@ struct EmojiInteractionsBunch { std::vector interactions; }; +struct EmojiInteractionSeen { + not_null peer; + not_null emoji; +}; + class EmojiInteractions final { public: explicit EmojiInteractions(not_null session); @@ -57,6 +62,11 @@ public: const QString &emoticon, EmojiInteractionsBunch &&bunch); + void seenOutgoing(not_null peer, const QString &emoticon); + [[nodiscard]] rpl::producer seen() const { + return _seen.events(); + } + [[nodiscard]] rpl::producer playRequests() const { return _playRequests.events(); } @@ -68,7 +78,7 @@ public: private: struct Animation { - EmojiPtr emoji; + not_null emoji; not_null document; std::shared_ptr media; crl::time scheduledAt = 0; @@ -76,6 +86,10 @@ private: bool incoming = false; int index = 0; }; + struct PlaySent { + mtpRequestId lastRequestId = 0; + crl::time lastDoneReceivedAt = 0; + }; struct CheckResult { crl::time nextCheckAt = 0; bool waitingForDownload = false; @@ -98,6 +112,7 @@ private: void setWaitingForDownload(bool waiting); void checkSeenRequests(crl::time now); + void checkSentRequests(crl::time now); void checkEdition( not_null item, base::flat_map, std::vector> &map); @@ -111,6 +126,10 @@ private: base::flat_map< not_null, base::flat_map> _playStarted; + base::flat_map< + not_null, + base::flat_map, PlaySent>> _playsSent; + rpl::event_stream _seen; bool _waitingForDownload = false; rpl::lifetime _downloadCheckLifetime; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index db0d1a65f9..4cbc910169 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -29,6 +29,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/radial_animation.h" #include "ui/toasts/common_toasts.h" #include "ui/boxes/report_box.h" // Ui::ReportReason +#include "ui/text/text.h" +#include "ui/text/text_options.h" #include "ui/special_buttons.h" #include "ui/unread_badge.h" #include "ui/ui_utility.h" @@ -45,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_changes.h" #include "data/data_send_action.h" +#include "chat_helpers/emoji_interactions.h" #include "base/unixtime.h" #include "support/support_helper.h" #include "apiwrap.h" @@ -54,6 +57,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_info.h" namespace HistoryView { +namespace { + +constexpr auto kEmojiInteractionSeenDuration = 3 * crl::time(1000); + +} // namespace + +struct TopBarWidget::EmojiInteractionSeenAnimation { + Ui::SendActionAnimation animation; + Ui::Animations::Basic scheduler; + Ui::Text::String text = { st::dialogsTextWidthMin }; + crl::time till = 0; +}; TopBarWidget::TopBarWidget( QWidget *parent, @@ -402,6 +417,7 @@ void TopBarWidget::paintTopBar(Painter &p) { return; } + const auto now = crl::now(); const auto history = _activeChat.key.history(); const auto folder = _activeChat.key.folder(); if (folder @@ -442,14 +458,14 @@ void TopBarWidget::paintTopBar(Painter &p) { p.setFont(st::dialogsTextFont); if (!paintConnectingState(p, nameleft, statustop, width()) - && !_sendAction->paint( + && !paintSendAction( p, nameleft, statustop, availableWidth, width(), st::historyStatusFgTyping, - crl::now())) { + now)) { p.setPen(st::historyStatusFg); p.drawTextLeft(nameleft, statustop, width(), _customTitleText); } @@ -481,19 +497,48 @@ void TopBarWidget::paintTopBar(Painter &p) { p.setFont(st::dialogsTextFont); if (!paintConnectingState(p, nameleft, statustop, width()) - && !_sendAction->paint( + && !paintSendAction( p, nameleft, statustop, availableWidth, width(), st::historyStatusFgTyping, - crl::now())) { + now)) { paintStatus(p, nameleft, statustop, availableWidth, width()); } } } +bool TopBarWidget::paintSendAction( + Painter &p, + int x, + int y, + int availableWidth, + int outerWidth, + style::color fg, + crl::time now) { + const auto seen = _emojiInteractionSeen.get(); + if (!seen || seen->till <= now) { + return _sendAction->paint(p, x, y, availableWidth, outerWidth, fg, now); + } + const auto animationWidth = seen->animation.width(); + const auto extraAnimationWidth = animationWidth * 2; + seen->animation.paint( + p, + fg, + x, + y + st::normalFont->ascent, + outerWidth, + now); + + x += animationWidth; + availableWidth -= extraAnimationWidth; + p.setPen(fg); + seen->text.drawElided(p, x, y, availableWidth); + return true; +} + bool TopBarWidget::paintConnectingState( Painter &p, int left, @@ -597,6 +642,7 @@ void TopBarWidget::setActiveChat( update(); if (peerChanged) { + _emojiInteractionSeen = nullptr; _activeChatLifetime.destroy(); if (const auto history = _activeChat.key.history()) { session().changes().peerFlagsValue( @@ -615,6 +661,14 @@ void TopBarWidget::setActiveChat( updateControlsVisibility(); updateControlsGeometry(); }, _activeChatLifetime); + + using InteractionSeen = ChatHelpers::EmojiInteractionSeen; + _controller->emojiInteractions().seen( + ) | rpl::filter([=](const InteractionSeen &seen) { + return (seen.peer == history->peer); + }) | rpl::start_with_next([=](const InteractionSeen &seen) { + handleEmojiInteractionSeen(seen.emoji->text()); + }, lifetime()); } } updateUnreadBadge(); @@ -628,6 +682,42 @@ void TopBarWidget::setActiveChat( refreshUnreadBadge(); } +void TopBarWidget::handleEmojiInteractionSeen(const QString &emoticon) { + auto seen = _emojiInteractionSeen.get(); + if (!seen) { + _emojiInteractionSeen + = std::make_unique(); + seen = _emojiInteractionSeen.get(); + seen->animation.start(Ui::SendActionAnimation::Type::ChooseSticker); + seen->scheduler.init([=] { + if (seen->till <= crl::now()) { + crl::on_main(this, [=] { + if (_emojiInteractionSeen + && _emojiInteractionSeen->till <= crl::now()) { + _emojiInteractionSeen = nullptr; + update(); + } + }); + } else { + const auto animationWidth = seen->animation.width(); + const auto skip = st::topBarArrowPadding.bottom(); + update( + _leftTaken, + st::topBarHeight - skip - st::dialogsTextFont->height, + seen->animation.width(), + st::dialogsTextFont->height); + } + }); + seen->scheduler.start(); + } + seen->till = crl::now() + kEmojiInteractionSeenDuration; + seen->text.setText( + st::dialogsTextStyle, + tr::lng_user_action_watching_animations(tr::now, lt_emoji, emoticon), + Ui::NameTextOptions()); + update(); +} + void TopBarWidget::setCustomTitle(const QString &title) { if (_customTitleText != title) { _customTitleText = title; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h index 9bd045a998..ae4a2b382a 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h @@ -95,6 +95,8 @@ protected: int resizeGetHeight(int newWidth) override; private: + struct EmojiInteractionSeenAnimation; + void refreshInfoButton(); void refreshLang(); void updateSearchVisibility(); @@ -109,6 +111,16 @@ private: void showMenu(); void toggleInfoSection(); + void handleEmojiInteractionSeen(const QString &emoticon); + bool paintSendAction( + Painter &p, + int x, + int y, + int availableWidth, + int outerWidth, + style::color fg, + crl::time now); + void updateConnectingState(); void updateAdaptiveLayout(); int countSelectedButtonsTop(float64 selectedShown); @@ -140,6 +152,7 @@ private: const not_null _controller; ActiveChat _activeChat; QString _customTitleText; + std::unique_ptr _emojiInteractionSeen; rpl::lifetime _activeChatLifetime; int _selectedCount = 0;