1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "history/view/history_view_emoji_interactions.h"
9 
10 #include "history/view/history_view_element.h"
11 #include "history/view/media/history_view_sticker.h"
12 #include "history/history.h"
13 #include "chat_helpers/emoji_interactions.h"
14 #include "chat_helpers/stickers_lottie.h"
15 #include "main/main_session.h"
16 #include "data/data_session.h"
17 #include "data/data_document.h"
18 #include "data/data_document_media.h"
19 #include "lottie/lottie_common.h"
20 #include "lottie/lottie_single_player.h"
21 #include "base/random.h"
22 #include "styles/style_chat.h"
23 
24 namespace HistoryView {
25 namespace {
26 
27 constexpr auto kSizeMultiplier = 3;
28 constexpr auto kCachesCount = 4;
29 constexpr auto kMaxPlays = 5;
30 constexpr auto kMaxPlaysWithSmallDelay = 3;
31 constexpr auto kSmallDelay = crl::time(200);
32 constexpr auto kDropDelayedAfterDelay = crl::time(2000);
33 
GenerateRandomShift(QSize emoji)34 [[nodiscard]] QPoint GenerateRandomShift(QSize emoji) {
35 	// Random shift in [-0.08 ... 0.08] of animated emoji size.
36 	const auto maxShift = emoji * 2 / 25;
37 	return {
38 		base::RandomIndex(maxShift.width() * 2 + 1) - maxShift.width(),
39 		base::RandomIndex(maxShift.height() * 2 + 1) - maxShift.height(),
40 	};
41 }
42 
43 } // namespace
44 
EmojiInteractions(not_null<Main::Session * > session)45 EmojiInteractions::EmojiInteractions(not_null<Main::Session*> session)
46 : _session(session) {
47 	_session->data().viewRemoved(
48 	) | rpl::filter([=] {
49 		return !_plays.empty() || !_delayed.empty();
50 	}) | rpl::start_with_next([=](not_null<const Element*> view) {
51 		_plays.erase(ranges::remove(_plays, view, &Play::view), end(_plays));
52 		_delayed.erase(
53 			ranges::remove(_delayed, view, &Delayed::view),
54 			end(_delayed));
55 	}, _lifetime);
56 
57 	_emojiSize = Sticker::EmojiSize();
58 }
59 
60 EmojiInteractions::~EmojiInteractions() = default;
61 
play(ChatHelpers::EmojiInteractionPlayRequest request,not_null<Element * > view)62 void EmojiInteractions::play(
63 		ChatHelpers::EmojiInteractionPlayRequest request,
64 		not_null<Element*> view) {
65 	if (!view->media()) {
66 		// Large emoji may be disabled.
67 		return;
68 	} else if (_plays.empty()) {
69 		play(
70 			std::move(request.emoticon),
71 			view,
72 			std::move(request.media),
73 			request.incoming);
74 	} else {
75 		const auto now = crl::now();
76 		_delayed.push_back({
77 			request.emoticon,
78 			view,
79 			std::move(request.media),
80 			now,
81 			request.incoming,
82 		});
83 		checkDelayed();
84 	}
85 }
86 
play(QString emoticon,not_null<Element * > view,std::shared_ptr<Data::DocumentMedia> media,bool incoming)87 void EmojiInteractions::play(
88 		QString emoticon,
89 		not_null<Element*> view,
90 		std::shared_ptr<Data::DocumentMedia> media,
91 		bool incoming) {
92 	const auto top = view->block()->y() + view->y();
93 	const auto bottom = top + view->height();
94 	if (_visibleTop >= bottom
95 		|| _visibleBottom <= top
96 		|| _visibleTop == _visibleBottom) {
97 		return;
98 	}
99 
100 	auto lottie = preparePlayer(media.get());
101 
102 	const auto shift = GenerateRandomShift(_emojiSize);
103 	lottie->updates(
104 	) | rpl::start_with_next([=](Lottie::Update update) {
105 		v::match(update.data, [&](const Lottie::Information &information) {
106 		}, [&](const Lottie::DisplayFrameRequest &request) {
107 			const auto rect = computeRect(view).translated(shift);
108 			if (rect.y() + rect.height() >= _visibleTop
109 				&& rect.y() <= _visibleBottom) {
110 				_updateRequests.fire_copy(rect);
111 			}
112 		});
113 	}, lottie->lifetime());
114 	_plays.push_back({
115 		.view = view,
116 		.lottie = std::move(lottie),
117 		.shift = shift,
118 	});
119 	if (incoming) {
120 		_playStarted.fire(std::move(emoticon));
121 	}
122 	if (const auto media = view->media()) {
123 		media->stickerClearLoopPlayed();
124 	}
125 }
126 
preparePlayer(not_null<Data::DocumentMedia * > media)127 std::unique_ptr<Lottie::SinglePlayer> EmojiInteractions::preparePlayer(
128 		not_null<Data::DocumentMedia*> media) {
129 	// Shortened copy from stickers_lottie module.
130 	const auto document = media->owner();
131 	const auto baseKey = document->bigFileBaseCacheKey();
132 	const auto tag = uint8(0);
133 	const auto keyShift = ((tag << 4) & 0xF0)
134 		| (uint8(ChatHelpers::StickerLottieSize::EmojiInteraction) & 0x0F);
135 	const auto key = Storage::Cache::Key{
136 		baseKey.high,
137 		baseKey.low + keyShift
138 	};
139 	const auto get = [=](int i, FnMut<void(QByteArray &&cached)> handler) {
140 		document->owner().cacheBigFile().get(
141 			{ key.high, key.low + i },
142 			std::move(handler));
143 	};
144 	const auto weak = base::make_weak(&document->session());
145 	const auto put = [=](int i, QByteArray &&cached) {
146 		crl::on_main(weak, [=, data = std::move(cached)]() mutable {
147 			weak->data().cacheBigFile().put(
148 				{ key.high, key.low + i },
149 				std::move(data));
150 		});
151 	};
152 	const auto data = media->bytes();
153 	const auto filepath = document->filepath();
154 	const auto request = Lottie::FrameRequest{
155 		_emojiSize * kSizeMultiplier * style::DevicePixelRatio(),
156 	};
157 	auto &weakProvider = _sharedProviders[document];
158 	auto shared = [&] {
159 		if (const auto result = weakProvider.lock()) {
160 			return result;
161 		}
162 		const auto result = Lottie::SinglePlayer::SharedProvider(
163 			kCachesCount,
164 			get,
165 			put,
166 			Lottie::ReadContent(data, filepath),
167 			request,
168 			Lottie::Quality::High);
169 		weakProvider = result;
170 		return result;
171 	}();
172 	return std::make_unique<Lottie::SinglePlayer>(std::move(shared), request);
173 }
174 
visibleAreaUpdated(int visibleTop,int visibleBottom)175 void EmojiInteractions::visibleAreaUpdated(
176 		int visibleTop,
177 		int visibleBottom) {
178 	_visibleTop = visibleTop;
179 	_visibleBottom = visibleBottom;
180 }
181 
computeRect(not_null<Element * > view) const182 QRect EmojiInteractions::computeRect(not_null<Element*> view) const {
183 	const auto fullWidth = view->width();
184 	const auto shift = (_emojiSize.width() * kSizeMultiplier) / 40;
185 	const auto skip = (view->hasFromPhoto() ? st::msgPhotoSkip : 0)
186 		+ st::msgMargin.left();
187 	const auto rightAligned = view->hasOutLayout()
188 		&& !view->delegate()->elementIsChatWide();
189 	const auto left = rightAligned
190 		? (fullWidth - skip + shift - _emojiSize.width() * kSizeMultiplier)
191 		: (skip - shift);
192 	const auto viewTop = view->block()->y() + view->y() + view->marginTop();
193 	const auto top = viewTop - _emojiSize.height();
194 	return QRect(QPoint(left, top), _emojiSize * kSizeMultiplier);
195 }
196 
paint(QPainter & p)197 void EmojiInteractions::paint(QPainter &p) {
198 	const auto factor = style::DevicePixelRatio();
199 	for (auto &play : _plays) {
200 		if (!play.lottie->ready()) {
201 			continue;
202 		}
203 		auto request = Lottie::FrameRequest();
204 		request.box = _emojiSize * kSizeMultiplier * factor;
205 		const auto rightAligned = play.view->hasOutLayout()
206 			&& !play.view->delegate()->elementIsChatWide();
207 		if (!rightAligned) {
208 			request.mirrorHorizontal = true;
209 		}
210 		const auto frame = play.lottie->frameInfo(request);
211 		play.frame = frame.index;
212 		if (!play.framesCount) {
213 			const auto &information = play.lottie->information();
214 			play.framesCount = information.framesCount;
215 			play.frameRate = information.frameRate;
216 		}
217 		if (play.frame + 1 == play.framesCount) {
218 			play.finished = true;
219 		}
220 		const auto rect = computeRect(play.view);
221 		p.drawImage(
222 			QRect(rect.topLeft() + play.shift, frame.image.size() / factor),
223 			frame.image);
224 		play.lottie->markFrameShown();
225 	}
226 	_plays.erase(ranges::remove(_plays, true, &Play::finished), end(_plays));
227 	checkDelayed();
228 }
229 
checkDelayed()230 void EmojiInteractions::checkDelayed() {
231 	if (_delayed.empty() || _plays.size() >= kMaxPlays) {
232 		return;
233 	}
234 	auto withTooLittleDelay = false;
235 	auto withHalfPlayed = false;
236 	for (const auto &play : _plays) {
237 		if (!play.framesCount
238 			|| !play.frameRate
239 			|| !play.frame
240 			|| (play.frame * crl::time(1000)
241 				< kSmallDelay * play.frameRate)) {
242 			withTooLittleDelay = true;
243 			break;
244 		} else if (play.frame * 2 > play.framesCount) {
245 			withHalfPlayed = true;
246 		}
247 	}
248 	if (withTooLittleDelay) {
249 		return;
250 	} else if (_plays.size() >= kMaxPlaysWithSmallDelay && !withHalfPlayed) {
251 		return;
252 	}
253 	const auto now = crl::now();
254 	const auto i = ranges::find_if(_delayed, [&](const Delayed &delayed) {
255 		return (delayed.shouldHaveStartedAt + kDropDelayedAfterDelay > now);
256 	});
257 	if (i == end(_delayed)) {
258 		_delayed.clear();
259 		return;
260 	}
261 	auto good = std::move(*i);
262 	_delayed.erase(begin(_delayed), i + 1);
263 	play(
264 		std::move(good.emoticon),
265 		good.view,
266 		std::move(good.media),
267 		good.incoming);
268 }
269 
updateRequests() const270 rpl::producer<QRect> EmojiInteractions::updateRequests() const {
271 	return _updateRequests.events();
272 }
273 
playStarted() const274 rpl::producer<QString> EmojiInteractions::playStarted() const {
275 	return _playStarted.events();
276 }
277 
278 } // namespace HistoryView
279