526 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			526 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
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_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 kAccumulateSeenRequests = kAccumulateDelay;
 | 
						|
constexpr auto kAcceptSeenSinceRequest = 3 * crl::time(1000);
 | 
						|
constexpr auto kMaxDelay = 2 * crl::time(1000);
 | 
						|
constexpr auto kTimeNever = std::numeric_limits<crl::time>::max();
 | 
						|
constexpr auto kJsonVersion = 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
 | 
						|
		| Data::MessageUpdate::Flag::Edited
 | 
						|
	) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
 | 
						|
		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::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 != chooseInteractionEmoji(item))) {
 | 
						|
		map.erase(i);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
EmojiPtr EmojiInteractions::chooseInteractionEmoji(
 | 
						|
		not_null<HistoryItem*> item) const {
 | 
						|
	return chooseInteractionEmoji(item->originalText().text);
 | 
						|
}
 | 
						|
 | 
						|
EmojiPtr EmojiInteractions::chooseInteractionEmoji(
 | 
						|
		const QString &emoticon) const {
 | 
						|
	const auto emoji = Ui::Emoji::Find(emoticon);
 | 
						|
	if (!emoji) {
 | 
						|
		return nullptr;
 | 
						|
	}
 | 
						|
	const auto &pack = _session->emojiStickersPack();
 | 
						|
	if (!pack.animationsForEmoji(emoji).empty()) {
 | 
						|
		return emoji;
 | 
						|
	}
 | 
						|
	if (const auto original = emoji->original(); original != emoji) {
 | 
						|
		if (!pack.animationsForEmoji(original).empty()) {
 | 
						|
			return original;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	static const auto kHearts = {
 | 
						|
		QString::fromUtf8("\xf0\x9f\x92\x9b"),
 | 
						|
		QString::fromUtf8("\xf0\x9f\x92\x99"),
 | 
						|
		QString::fromUtf8("\xf0\x9f\x92\x9a"),
 | 
						|
		QString::fromUtf8("\xf0\x9f\x92\x9c"),
 | 
						|
		QString::fromUtf8("\xf0\x9f\xa7\xa1"),
 | 
						|
		QString::fromUtf8("\xf0\x9f\x96\xa4"),
 | 
						|
		QString::fromUtf8("\xf0\x9f\xa4\x8e"),
 | 
						|
		QString::fromUtf8("\xf0\x9f\xa4\x8d"),
 | 
						|
	};
 | 
						|
	return ranges::contains(kHearts, emoji->id())
 | 
						|
		? Ui::Emoji::Find(QString::fromUtf8("\xe2\x9d\xa4"))
 | 
						|
		: emoji;
 | 
						|
}
 | 
						|
 | 
						|
void EmojiInteractions::startOutgoing(
 | 
						|
		not_null<const HistoryView::Element*> view) {
 | 
						|
	const auto item = view->data();
 | 
						|
	if (!item->isRegular() || !item->history()->peer->isUser()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto emoticon = item->originalText().text;
 | 
						|
	const auto emoji = chooseInteractionEmoji(emoticon);
 | 
						|
	if (!emoji) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto &pack = _session->emojiStickersPack();
 | 
						|
	const auto &list = pack.animationsForEmoji(emoji);
 | 
						|
	if (list.empty()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	auto &animations = _outgoing[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({
 | 
						|
		.emoticon = emoticon,
 | 
						|
		.emoji = emoji,
 | 
						|
		.document = document,
 | 
						|
		.media = media,
 | 
						|
		.scheduledAt = now,
 | 
						|
		.index = index,
 | 
						|
	});
 | 
						|
	check(now);
 | 
						|
}
 | 
						|
 | 
						|
void EmojiInteractions::startIncoming(
 | 
						|
		not_null<PeerData*> peer,
 | 
						|
		MsgId messageId,
 | 
						|
		const QString &emoticon,
 | 
						|
		EmojiInteractionsBunch &&bunch) {
 | 
						|
	if (!peer->isUser() || bunch.interactions.empty()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto item = _session->data().message(peer->id, messageId);
 | 
						|
	if (!item || !item->isRegular()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto emoji = chooseInteractionEmoji(item);
 | 
						|
	if (!emoji || emoji != chooseInteractionEmoji(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(base::SafeRound(single.time * 1000));
 | 
						|
		if (!animations.empty() && animations.back().scheduledAt >= at) {
 | 
						|
			continue;
 | 
						|
		}
 | 
						|
		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({
 | 
						|
				.emoticon = emoticon,
 | 
						|
				.emoji = emoji,
 | 
						|
				.document = document,
 | 
						|
				.media = media,
 | 
						|
				.scheduledAt = at,
 | 
						|
				.incoming = true,
 | 
						|
				.index = index,
 | 
						|
			});
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (animations.empty()) {
 | 
						|
		_incoming.remove(item);
 | 
						|
	} else {
 | 
						|
		check(now);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void EmojiInteractions::seenOutgoing(
 | 
						|
		not_null<PeerData*> peer,
 | 
						|
		const QString &emoticon) {
 | 
						|
	if (const auto i = _playsSent.find(peer); i != end(_playsSent)) {
 | 
						|
		if (const auto emoji = chooseInteractionEmoji(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, emoticon });
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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 i = begin(map); i != end(map);) {
 | 
						|
		auto lastStartedAt = crl::time();
 | 
						|
 | 
						|
		auto &animations = i->second;
 | 
						|
		// Erase too old requests.
 | 
						|
		const auto j = ranges::find_if(animations, [&](const Animation &a) {
 | 
						|
			return !a.startedAt && (a.scheduledAt + kMaxDelay <= now);
 | 
						|
		});
 | 
						|
		if (j == begin(animations)) {
 | 
						|
			i = map.erase(i);
 | 
						|
			continue;
 | 
						|
		} else if (j != end(animations)) {
 | 
						|
			animations.erase(j, end(animations));
 | 
						|
		}
 | 
						|
		const auto item = i->first;
 | 
						|
		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;
 | 
						|
				_playRequests.fire({
 | 
						|
					animation.emoticon,
 | 
						|
					item,
 | 
						|
					animation.media,
 | 
						|
					animation.scheduledAt,
 | 
						|
					animation.incoming,
 | 
						|
				});
 | 
						|
				break;
 | 
						|
			} else {
 | 
						|
				nearest = std::min(nearest, lastStartedAt + kMinDelay);
 | 
						|
				break;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		++i;
 | 
						|
	}
 | 
						|
	return {
 | 
						|
		.nextCheckAt = nearest,
 | 
						|
		.waitingForDownload = waitingForDownload,
 | 
						|
	};
 | 
						|
}
 | 
						|
 | 
						|
void EmojiInteractions::sendAccumulatedOutgoing(
 | 
						|
		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 bunch = EmojiInteractionsBunch();
 | 
						|
	bunch.interactions.reserve(till - from);
 | 
						|
	for (const auto &animation : ranges::make_subrange(from, till)) {
 | 
						|
		bunch.interactions.push_back({
 | 
						|
			.index = animation.index + 1,
 | 
						|
			.time = (animation.startedAt - firstStartedAt) / 1000.,
 | 
						|
		});
 | 
						|
	}
 | 
						|
	if (bunch.interactions.empty()) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto peer = item->history()->peer;
 | 
						|
	const auto emoji = from->emoji;
 | 
						|
	const auto requestId = _session->api().request(MTPmessages_SetTyping(
 | 
						|
		MTP_flags(0),
 | 
						|
		peer->input,
 | 
						|
		MTPint(), // top_msg_id
 | 
						|
		MTP_sendMessageEmojiInteraction(
 | 
						|
			MTP_string(from->emoticon),
 | 
						|
			MTP_int(item->id),
 | 
						|
			MTP_dataJSON(MTP_bytes(ToJson(bunch))))
 | 
						|
	)).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);
 | 
						|
}
 | 
						|
 | 
						|
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(_outgoing); i != end(_outgoing);) {
 | 
						|
		auto &[item, animations] = *i;
 | 
						|
		sendAccumulatedOutgoing(now, item, animations);
 | 
						|
		if (animations.empty()) {
 | 
						|
			i = _outgoing.erase(i);
 | 
						|
			continue;
 | 
						|
		} else if (const auto firstStartedAt = animations.front().startedAt) {
 | 
						|
			nearest = std::min(nearest, firstStartedAt + kAccumulateDelay);
 | 
						|
			Assert(nearest > now);
 | 
						|
		}
 | 
						|
		++i;
 | 
						|
	}
 | 
						|
	for (auto i = begin(_incoming); i != end(_incoming);) {
 | 
						|
		auto &animations = i->second;
 | 
						|
		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,
 | 
						|
	};
 | 
						|
}
 | 
						|
 | 
						|
void EmojiInteractions::check(crl::time now) {
 | 
						|
	if (!now) {
 | 
						|
		now = crl::now();
 | 
						|
	}
 | 
						|
	checkSeenRequests(now);
 | 
						|
	checkSentRequests(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);
 | 
						|
	} else if (!_playStarted.empty()) {
 | 
						|
		_checkTimer.callOnce(kAccumulateSeenRequests);
 | 
						|
	} else if (!_playsSent.empty()) {
 | 
						|
		_checkTimer.callOnce(kAcceptSeenSinceRequest);
 | 
						|
	}
 | 
						|
	setWaitingForDownload(result.waitingForDownload);
 | 
						|
}
 | 
						|
 | 
						|
void EmojiInteractions::checkSeenRequests(crl::time now) {
 | 
						|
	for (auto i = begin(_playStarted); i != end(_playStarted);) {
 | 
						|
		auto &animations = i->second;
 | 
						|
		for (auto j = begin(animations); j != end(animations);) {
 | 
						|
			if (j->second + kAccumulateSeenRequests <= now) {
 | 
						|
				j = animations.erase(j);
 | 
						|
			} else {
 | 
						|
				++j;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if (animations.empty()) {
 | 
						|
			i = _playStarted.erase(i);
 | 
						|
		} else {
 | 
						|
			++i;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void EmojiInteractions::checkSentRequests(crl::time now) {
 | 
						|
	for (auto i = begin(_playsSent); i != end(_playsSent);) {
 | 
						|
		auto &animations = i->second;
 | 
						|
		for (auto j = begin(animations); j != end(animations);) {
 | 
						|
			const auto last = j->second.lastDoneReceivedAt;
 | 
						|
			if (last && last + kAcceptSeenSinceRequest <= now) {
 | 
						|
				j = animations.erase(j);
 | 
						|
			} else {
 | 
						|
				++j;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if (animations.empty()) {
 | 
						|
			i = _playsSent.erase(i);
 | 
						|
		} else {
 | 
						|
			++i;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void EmojiInteractions::playStarted(not_null<PeerData*> peer, QString emoji) {
 | 
						|
	auto &map = _playStarted[peer];
 | 
						|
	const auto i = map.find(emoji);
 | 
						|
	const auto now = crl::now();
 | 
						|
	if (i != end(map) && now - i->second < kAccumulateSeenRequests) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	_session->api().request(MTPmessages_SetTyping(
 | 
						|
		MTP_flags(0),
 | 
						|
		peer->input,
 | 
						|
		MTPint(), // top_msg_id
 | 
						|
		MTP_sendMessageEmojiInteractionSeen(MTP_string(emoji))
 | 
						|
	)).send();
 | 
						|
	map[emoji] = now;
 | 
						|
	if (!_checkTimer.isActive()) {
 | 
						|
		_checkTimer.callOnce(kAccumulateSeenRequests);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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
 |