Accumulate and send emoji interactions.

This commit is contained in:
John Preston 2021-09-14 23:42:34 +03:00
parent d152782115
commit 139b9723d7
9 changed files with 324 additions and 12 deletions

View File

@ -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

View File

@ -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 <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 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 &[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<HistoryView::Sticker>(
// view,
// document);
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 &[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

View File

@ -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<Main::Session*> session);
~EmojiInteractions();
void start(not_null<const HistoryView::Element*> view);
private:
struct Animation {
EmojiPtr emoji;
not_null<DocumentData*> document;
std::shared_ptr<Data::DocumentMedia> 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<HistoryItem*> item,
std::vector<Animation> &animations);
void setWaitingForDownload(bool waiting);
const not_null<Main::Session*> _session;
base::flat_map<
not_null<HistoryItem*>,
std::vector<Animation>> _animations;
base::Timer _checkTimer;
bool _waitingForDownload = false;
rpl::lifetime _downloadCheckLifetime;
rpl::lifetime _lifetime;
};
} // namespace ChatHelpers

View File

@ -32,14 +32,14 @@ constexpr auto kRefreshTimeout = 7200 * crl::time(1000);
[[nodiscard]] std::optional<int> 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() {

View File

@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/history_inner_widget.h"
#include <rpl/merge.h>
#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<const Element*> view) {
_controller->emojiInteractions().start(view);
}
auto HistoryInner::getSelectionState() const

View File

@ -295,14 +295,9 @@ void Sticker::refreshLink() {
if (isEmojiSticker()) {
const auto weak = base::make_weak(this);
_link = std::make_shared<LambdaClickHandler>([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<LambdaClickHandler>([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;

View File

@ -97,6 +97,7 @@ private:
void setupLottie();
void lottieCreated();
void unloadLottie();
void emojiStickerClicked();
const not_null<Element*> _parent;
const not_null<DocumentData*> _data;

View File

@ -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<Controller*> window)
: SessionNavigation(session)
, _window(window)
, _emojiInteractions(
std::make_unique<ChatHelpers::EmojiInteractions>(session))
, _tabbedSelector(
std::make_unique<ChatHelpers::TabbedSelector>(
_window->widget(),

View File

@ -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<MainWidget*> 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<Controller*> _window;
const std::unique_ptr<ChatHelpers::EmojiInteractions> _emojiInteractions;
std::unique_ptr<Passport::FormController> _passportForm;
std::unique_ptr<FiltersMenu> _filters;