Play incoming interactions.

This commit is contained in:
John Preston 2021-09-16 14:20:25 +03:00
parent 703ea9aacd
commit cfb43081c7
7 changed files with 293 additions and 54 deletions

View File

@ -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<PeerData*> 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<PeerData*> 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: {

View File

@ -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<PeerData*> peer,
PeerId participantPeerId,
PeerData *participantPeerLoaded);
void handleEmojiInteraction(
not_null<PeerData*> peer,
MsgId messageId,
const QString &emoticon,
ChatHelpers::EmojiInteractionsBunch bunch);
const not_null<Main::Session*> _session;

View File

@ -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<crl::time>::max();
constexpr auto kVersion = 1;
constexpr auto kJsonVersion = 1;
} // namespace
@ -49,14 +50,32 @@ EmojiInteractions::EmojiInteractions(not_null<Main::Session*> 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<const HistoryView::Element*> view) {
void EmojiInteractions::checkEdition(
not_null<HistoryItem*> item,
base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &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<const HistoryView::Element*> view) {
const auto item = view->data();
if (!IsServerMsgId(item->id) || !item->history()->peer->isUser()) {
return;
@ -70,7 +89,7 @@ void EmojiInteractions::start(not_null<const HistoryView::Element*> 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<const HistoryView::Element*> view) {
check(now);
}
void EmojiInteractions::startIncoming(
not_null<PeerData*> 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<not_null<HistoryItem*>, std::vector<Animation>> &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<HistoryItem*> item,
std::vector<Animation> &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<Animation> &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

View File

@ -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<Single> interactions;
};
class EmojiInteractions final {
public:
explicit EmojiInteractions(not_null<Main::Session*> session);
@ -39,11 +48,21 @@ public:
using PlayRequest = EmojiInteractionPlayRequest;
void start(not_null<const HistoryView::Element*> view);
void startOutgoing(not_null<const HistoryView::Element*> view);
void startIncoming(
not_null<PeerData*> peer,
MsgId messageId,
const QString &emoticon,
EmojiInteractionsBunch &&bunch);
[[nodiscard]] rpl::producer<PlayRequest> 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<not_null<HistoryItem*>, std::vector<Animation>> &map);
[[nodiscard]] CheckResult checkAccumulated(crl::time now);
void sendAccumulated(
void sendAccumulatedOutgoing(
crl::time now,
not_null<HistoryItem*> item,
std::vector<Animation> &animations);
void clearAccumulatedIncoming(
crl::time now,
std::vector<Animation> &animations);
void setWaitingForDownload(bool waiting);
void checkEdition(
not_null<HistoryItem*> item,
base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map);
const not_null<Main::Session*> _session;
base::flat_map<
not_null<HistoryItem*>,
std::vector<Animation>> _animations;
base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> _outgoing;
base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> _incoming;
base::Timer _checkTimer;
rpl::event_stream<PlayRequest> _playRequests;

View File

@ -2720,7 +2720,7 @@ void HistoryInner::elementReplyTo(const FullMsgId &to) {
}
void HistoryInner::elementStartInteraction(not_null<const Element*> view) {
_controller->emojiInteractions().start(view);
_controller->emojiInteractions().startOutgoing(view);
}
auto HistoryInner::getSelectionState() const

View File

@ -100,6 +100,9 @@ void EmojiInteractions::play(
.lottie = std::move(lottie),
.shift = shift,
});
if (const auto media = view->media()) {
media->stickerClearLoopPlayed();
}
}
void EmojiInteractions::visibleAreaUpdated(

View File

@ -119,7 +119,7 @@ bool SendActionPainter::updateNeedsAnimating(
Type::ChooseSticker,
kStatusShowClientsideChooseSticker);
}, [&](const MTPDsendMessageEmojiInteraction &) {
// #TODO interaction
Unexpected("EmojiInteraction here.");
}, [&](const MTPDsendMessageEmojiInteractionSeen &) {
// #TODO interaction
}, [&](const MTPDsendMessageCancelAction &) {