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/history_item_components.h"
9 
10 #include "lang/lang_keys.h"
11 #include "ui/effects/ripple_animation.h"
12 #include "ui/image/image.h"
13 #include "ui/toast/toast.h"
14 #include "ui/text/text_options.h"
15 #include "ui/chat/chat_style.h"
16 #include "ui/chat/chat_theme.h"
17 #include "history/history.h"
18 #include "history/history_message.h"
19 #include "history/view/history_view_service_message.h"
20 #include "history/view/media/history_view_document.h"
21 #include "core/click_handler_types.h"
22 #include "layout/layout_position.h"
23 #include "mainwindow.h"
24 #include "media/audio/media_audio.h"
25 #include "media/player/media_player_instance.h"
26 #include "data/data_media_types.h"
27 #include "data/data_session.h"
28 #include "data/data_user.h"
29 #include "data/data_file_origin.h"
30 #include "data/data_document.h"
31 #include "data/data_file_click_handler.h"
32 #include "main/main_session.h"
33 #include "window/window_session_controller.h"
34 #include "facades.h"
35 #include "styles/style_widgets.h"
36 #include "styles/style_chat.h"
37 
38 #include <QtGui/QGuiApplication>
39 
40 namespace {
41 
42 const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_";
43 
44 } // namespace
45 
create(not_null<Data::Session * > owner,UserId userId)46 void HistoryMessageVia::create(
47 		not_null<Data::Session*> owner,
48 		UserId userId) {
49 	bot = owner->user(userId);
50 	maxWidth = st::msgServiceNameFont->width(
51 		tr::lng_inline_bot_via(tr::now, lt_inline_bot, '@' + bot->username));
52 	link = std::make_shared<LambdaClickHandler>([bot = this->bot](
53 			ClickContext context) {
54 		if (QGuiApplication::keyboardModifiers() == Qt::ControlModifier) {
55 			if (const auto window = App::wnd()) {
56 				if (const auto controller = window->sessionController()) {
57 					controller->showPeerInfo(bot);
58 					return;
59 				}
60 			}
61 		}
62 		const auto my = context.other.value<ClickHandlerContext>();
63 		if (const auto delegate = my.elementDelegate ? my.elementDelegate() : nullptr) {
64 			delegate->elementHandleViaClick(bot);
65 		} else {
66 			App::insertBotCommand('@' + bot->username);
67 		}
68 	});
69 }
70 
resize(int32 availw) const71 void HistoryMessageVia::resize(int32 availw) const {
72 	if (availw < 0) {
73 		text = QString();
74 		width = 0;
75 	} else {
76 		text = tr::lng_inline_bot_via(tr::now, lt_inline_bot, '@' + bot->username);
77 		if (availw < maxWidth) {
78 			text = st::msgServiceNameFont->elided(text, availw);
79 			width = st::msgServiceNameFont->width(text);
80 		} else if (width < maxWidth) {
81 			width = maxWidth;
82 		}
83 	}
84 }
85 
refresh(const QString & date)86 void HistoryMessageSigned::refresh(const QString &date) {
87 	Expects(!isAnonymousRank);
88 
89 	auto name = author;
90 	const auto time = qsl(", ") + date;
91 	const auto timew = st::msgDateFont->width(time);
92 	const auto namew = st::msgDateFont->width(name);
93 	isElided = (timew + namew > st::maxSignatureSize);
94 	if (isElided) {
95 		name = st::msgDateFont->elided(author, st::maxSignatureSize - timew);
96 	}
97 	signature.setText(
98 		st::msgDateTextStyle,
99 		name + time,
100 		Ui::NameTextOptions());
101 }
102 
maxWidth() const103 int HistoryMessageSigned::maxWidth() const {
104 	return signature.maxWidth();
105 }
106 
refresh(const QString & date,bool displayed)107 void HistoryMessageEdited::refresh(const QString &date, bool displayed) {
108 	const auto prefix = displayed
109 		? (tr::lng_edited(tr::now) + ' ')
110 		: QString();
111 	text.setText(st::msgDateTextStyle, prefix + date, Ui::NameTextOptions());
112 }
113 
maxWidth() const114 int HistoryMessageEdited::maxWidth() const {
115 	return text.maxWidth();
116 }
117 
HistoryMessageSponsored()118 HistoryMessageSponsored::HistoryMessageSponsored() {
119 	text.setText(
120 		st::msgDateTextStyle,
121 		tr::lng_sponsored(tr::now),
122 		Ui::NameTextOptions());
123 }
124 
maxWidth() const125 int HistoryMessageSponsored::maxWidth() const {
126 	return text.maxWidth();
127 }
128 
HiddenSenderInfo(const QString & name,bool external)129 HiddenSenderInfo::HiddenSenderInfo(const QString &name, bool external)
130 : name(name)
131 , colorPeerId(Data::FakePeerIdForJustName(name))
132 , userpic(
133 	Data::PeerUserpicColor(colorPeerId),
134 	(external
135 		? Ui::EmptyUserpic::ExternalName()
136 		: name)) {
137 	nameText.setText(st::msgNameStyle, name, Ui::NameTextOptions());
138 	const auto parts = name.trimmed().split(' ', Qt::SkipEmptyParts);
139 	firstName = parts[0];
140 	for (const auto &part : parts.mid(1)) {
141 		if (!lastName.isEmpty()) {
142 			lastName.append(' ');
143 		}
144 		lastName.append(part);
145 	}
146 }
147 
create(const HistoryMessageVia * via) const148 void HistoryMessageForwarded::create(const HistoryMessageVia *via) const {
149 	auto phrase = QString();
150 	const auto fromChannel = originalSender
151 		&& originalSender->isChannel()
152 		&& !originalSender->isMegagroup();
153 	const auto name = originalSender
154 		? originalSender->name
155 		: hiddenSenderInfo->name;
156 	if (!originalAuthor.isEmpty()) {
157 		phrase = tr::lng_forwarded_signed(
158 			tr::now,
159 			lt_channel,
160 			name,
161 			lt_user,
162 			originalAuthor);
163 	} else {
164 		phrase = name;
165 	}
166 	if (via && psaType.isEmpty()) {
167 		if (fromChannel) {
168 			phrase = tr::lng_forwarded_channel_via(
169 				tr::now,
170 				lt_channel,
171 				textcmdLink(1, phrase),
172 				lt_inline_bot,
173 				textcmdLink(2, '@' + via->bot->username));
174 		} else {
175 			phrase = tr::lng_forwarded_via(
176 				tr::now,
177 				lt_user,
178 				textcmdLink(1, phrase),
179 				lt_inline_bot,
180 				textcmdLink(2, '@' + via->bot->username));
181 		}
182 	} else {
183 		if (fromChannel || !psaType.isEmpty()) {
184 			auto custom = psaType.isEmpty()
185 				? QString()
186 				: Lang::GetNonDefaultValue(
187 					kPsaForwardedPrefix + psaType.toUtf8());
188 			phrase = !custom.isEmpty()
189 				? custom.replace("{channel}", textcmdLink(1, phrase))
190 				: (psaType.isEmpty()
191 					? tr::lng_forwarded_channel
192 					: tr::lng_forwarded_psa_default)(
193 						tr::now,
194 						lt_channel,
195 						textcmdLink(1, phrase));
196 		} else {
197 			phrase = tr::lng_forwarded(
198 				tr::now,
199 				lt_user,
200 				textcmdLink(1, phrase));
201 		}
202 	}
203 	TextParseOptions opts = {
204 		TextParseRichText,
205 		0,
206 		0,
207 		Qt::LayoutDirectionAuto
208 	};
209 	text.setText(st::fwdTextStyle, phrase, opts);
210 	static const auto hidden = std::make_shared<LambdaClickHandler>([] {
211 		Ui::Toast::Show(tr::lng_forwarded_hidden(tr::now));
212 	});
213 
214 	text.setLink(1, fromChannel
215 		? goToMessageClickHandler(originalSender, originalId)
216 		: originalSender
217 		? originalSender->openLink()
218 		: hidden);
219 	if (via) {
220 		text.setLink(2, via->link);
221 	}
222 }
223 
updateData(not_null<HistoryMessage * > holder,bool force)224 bool HistoryMessageReply::updateData(
225 		not_null<HistoryMessage*> holder,
226 		bool force) {
227 	const auto guard = gsl::finally([&] { refreshReplyToDocument(); });
228 	if (!force) {
229 		if (replyToMsg || !replyToMsgId) {
230 			return true;
231 		}
232 	}
233 	if (!replyToMsg) {
234 		replyToMsg = holder->history()->owner().message(
235 			(replyToPeerId
236 				? peerToChannel(replyToPeerId)
237 				: holder->channelId()),
238 			replyToMsgId);
239 		if (replyToMsg) {
240 			if (replyToMsg->isEmpty()) {
241 				// Really it is deleted.
242 				replyToMsg = nullptr;
243 				force = true;
244 			} else {
245 				holder->history()->owner().registerDependentMessage(
246 					holder,
247 					replyToMsg);
248 			}
249 		}
250 	}
251 
252 	if (replyToMsg) {
253 		replyToText.setText(
254 			st::messageTextStyle,
255 			replyToMsg->inReplyText(),
256 			Ui::DialogTextOptions());
257 
258 		updateName();
259 
260 		setReplyToLinkFrom(holder);
261 		if (!replyToMsg->Has<HistoryMessageForwarded>()) {
262 			if (auto bot = replyToMsg->viaBot()) {
263 				replyToVia = std::make_unique<HistoryMessageVia>();
264 				replyToVia->create(
265 					&holder->history()->owner(),
266 					peerToUser(bot->id));
267 			}
268 		}
269 	} else if (force) {
270 		replyToMsgId = 0;
271 	}
272 	if (force) {
273 		holder->history()->owner().requestItemResize(holder);
274 	}
275 	return (replyToMsg || !replyToMsgId);
276 }
277 
setReplyToLinkFrom(not_null<HistoryMessage * > holder)278 void HistoryMessageReply::setReplyToLinkFrom(
279 		not_null<HistoryMessage*> holder) {
280 	replyToLnk = replyToMsg
281 		? goToMessageClickHandler(replyToMsg, holder->fullId())
282 		: nullptr;
283 }
284 
clearData(not_null<HistoryMessage * > holder)285 void HistoryMessageReply::clearData(not_null<HistoryMessage*> holder) {
286 	replyToVia = nullptr;
287 	if (replyToMsg) {
288 		holder->history()->owner().unregisterDependentMessage(
289 			holder,
290 			replyToMsg);
291 		replyToMsg = nullptr;
292 	}
293 	replyToMsgId = 0;
294 	refreshReplyToDocument();
295 }
296 
isNameUpdated() const297 bool HistoryMessageReply::isNameUpdated() const {
298 	if (replyToMsg && replyToMsg->author()->nameVersion > replyToVersion) {
299 		updateName();
300 		return true;
301 	}
302 	return false;
303 }
304 
updateName() const305 void HistoryMessageReply::updateName() const {
306 	if (replyToMsg) {
307 		const auto from = [&] {
308 			if (const auto from = replyToMsg->displayFrom()) {
309 				return from;
310 			}
311 			return replyToMsg->author().get();
312 		}();
313 		const auto name = (replyToVia && from->isUser())
314 			? from->asUser()->firstName
315 			: from->name;
316 		replyToName.setText(st::fwdTextStyle, name, Ui::NameTextOptions());
317 		replyToVersion = replyToMsg->author()->nameVersion;
318 		bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false;
319 		int32 previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0;
320 		int32 w = replyToName.maxWidth();
321 		if (replyToVia) {
322 			w += st::msgServiceFont->spacew + replyToVia->maxWidth;
323 		}
324 
325 		maxReplyWidth = previewSkip + qMax(w, qMin(replyToText.maxWidth(), int32(st::maxSignatureSize)));
326 	} else {
327 		maxReplyWidth = st::msgDateFont->width(replyToMsgId ? tr::lng_profile_loading(tr::now) : tr::lng_deleted_message(tr::now));
328 	}
329 	maxReplyWidth = st::msgReplyPadding.left() + st::msgReplyBarSkip + maxReplyWidth + st::msgReplyPadding.right();
330 }
331 
resize(int width) const332 void HistoryMessageReply::resize(int width) const {
333 	if (replyToVia) {
334 		bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false;
335 		int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0;
336 		replyToVia->resize(width - st::msgReplyBarSkip - previewSkip - replyToName.maxWidth() - st::msgServiceFont->spacew);
337 	}
338 }
339 
itemRemoved(HistoryMessage * holder,HistoryItem * removed)340 void HistoryMessageReply::itemRemoved(
341 		HistoryMessage *holder,
342 		HistoryItem *removed) {
343 	if (replyToMsg == removed) {
344 		clearData(holder);
345 		holder->history()->owner().requestItemResize(holder);
346 	}
347 }
348 
paint(Painter & p,not_null<const HistoryView::Element * > holder,const Ui::ChatPaintContext & context,int x,int y,int w,bool inBubble) const349 void HistoryMessageReply::paint(
350 		Painter &p,
351 		not_null<const HistoryView::Element*> holder,
352 		const Ui::ChatPaintContext &context,
353 		int x,
354 		int y,
355 		int w,
356 		bool inBubble) const {
357 	const auto st = context.st;
358 	const auto stm = context.messageStyle();
359 
360 	const auto &bar = inBubble
361 		? stm->msgReplyBarColor
362 		: st->msgImgReplyBarColor();
363 	QRect rbar(style::rtlrect(x + st::msgReplyBarPos.x(), y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.width(), st::msgReplyBarSize.height(), w + 2 * x));
364 	p.fillRect(rbar, bar);
365 
366 	if (w > st::msgReplyBarSkip) {
367 		if (replyToMsg) {
368 			auto hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false;
369 			if (hasPreview && w < st::msgReplyBarSkip + st::msgReplyBarSize.height()) {
370 				hasPreview = false;
371 			}
372 			auto previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0;
373 
374 			if (hasPreview) {
375 				if (const auto image = replyToMsg->media()->replyPreview()) {
376 					auto to = style::rtlrect(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height(), w + 2 * x);
377 					auto previewWidth = image->width() / cIntRetinaFactor();
378 					auto previewHeight = image->height() / cIntRetinaFactor();
379 					auto preview = image->pixSingle(
380 						previewWidth,
381 						previewHeight,
382 						to.width(),
383 						to.height(),
384 						ImageRoundRadius::Small,
385 						RectPart::AllCorners,
386 						context.selected() ? &st->msgStickerOverlay() : nullptr);
387 					p.drawPixmap(to.x(), to.y(), preview);
388 				}
389 			}
390 			if (w > st::msgReplyBarSkip + previewSkip) {
391 				p.setPen(inBubble
392 					? stm->msgServiceFg
393 					: st->msgImgReplyBarColor());
394 				replyToName.drawLeftElided(p, x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top(), w - st::msgReplyBarSkip - previewSkip, w + 2 * x);
395 				if (replyToVia && w > st::msgReplyBarSkip + previewSkip + replyToName.maxWidth() + st::msgServiceFont->spacew) {
396 					p.setFont(st::msgServiceFont);
397 					p.drawText(x + st::msgReplyBarSkip + previewSkip + replyToName.maxWidth() + st::msgServiceFont->spacew, y + st::msgReplyPadding.top() + st::msgServiceFont->ascent, replyToVia->text);
398 				}
399 
400 				p.setPen(inBubble
401 					? stm->historyTextFg
402 					: st->msgImgReplyBarColor());
403 				p.setTextPalette(inBubble
404 					? stm->replyTextPalette
405 					: st->imgReplyTextPalette());
406 				replyToText.drawLeftElided(p, x + st::msgReplyBarSkip + previewSkip, y + st::msgReplyPadding.top() + st::msgServiceNameFont->height, w - st::msgReplyBarSkip - previewSkip, w + 2 * x);
407 				p.setTextPalette(stm->textPalette);
408 			}
409 		} else {
410 			p.setFont(st::msgDateFont);
411 			p.setPen(inBubble
412 				? stm->msgDateFg
413 				: st->msgDateImgFg());
414 			p.drawTextLeft(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2, w + 2 * x, st::msgDateFont->elided(replyToMsgId ? tr::lng_profile_loading(tr::now) : tr::lng_deleted_message(tr::now), w - st::msgReplyBarSkip));
415 		}
416 	}
417 }
418 
refreshReplyToDocument()419 void HistoryMessageReply::refreshReplyToDocument() {
420 	replyToDocumentId = 0;
421 	if (const auto media = replyToMsg ? replyToMsg->media() : nullptr) {
422 		if (const auto document = media->document()) {
423 			replyToDocumentId = document->id;
424 		}
425 	}
426 }
427 
ReplyMarkupClickHandler(not_null<Data::Session * > owner,int row,int column,FullMsgId context)428 ReplyMarkupClickHandler::ReplyMarkupClickHandler(
429 	not_null<Data::Session*> owner,
430 	int row,
431 	int column,
432 	FullMsgId context)
433 : _owner(owner)
434 , _itemId(context)
435 , _row(row)
436 , _column(column) {
437 }
438 
439 // Copy to clipboard support.
copyToClipboardText() const440 QString ReplyMarkupClickHandler::copyToClipboardText() const {
441 	const auto button = getUrlButton();
442 	return button ? QString::fromUtf8(button->data) : QString();
443 }
444 
copyToClipboardContextItemText() const445 QString ReplyMarkupClickHandler::copyToClipboardContextItemText() const {
446 	const auto button = getUrlButton();
447 	return button ? tr::lng_context_copy_link(tr::now) : QString();
448 }
449 
450 // Finds the corresponding button in the items markup struct.
451 // If the button is not found it returns nullptr.
452 // Note: it is possible that we will point to the different button
453 // than the one was used when constructing the handler, but not a big deal.
getButton() const454 const HistoryMessageMarkupButton *ReplyMarkupClickHandler::getButton() const {
455 	return HistoryMessageMarkupButton::Get(_owner, _itemId, _row, _column);
456 }
457 
getUrlButton() const458 auto ReplyMarkupClickHandler::getUrlButton() const
459 -> const HistoryMessageMarkupButton* {
460 	if (const auto button = getButton()) {
461 		using Type = HistoryMessageMarkupButton::Type;
462 		if (button->type == Type::Url || button->type == Type::Auth) {
463 			return button;
464 		}
465 	}
466 	return nullptr;
467 }
468 
onClick(ClickContext context) const469 void ReplyMarkupClickHandler::onClick(ClickContext context) const {
470 	if (context.button != Qt::LeftButton) {
471 		return;
472 	}
473 	if (const auto item = _owner->message(_itemId)) {
474 		const auto my = context.other.value<ClickHandlerContext>();
475 		App::activateBotCommand(my.sessionWindow.get(), item, _row, _column);
476 	}
477 }
478 
479 // Returns the full text of the corresponding button.
buttonText() const480 QString ReplyMarkupClickHandler::buttonText() const {
481 	if (const auto button = getButton()) {
482 		return button->text;
483 	}
484 	return QString();
485 }
486 
tooltip() const487 QString ReplyMarkupClickHandler::tooltip() const {
488 	const auto button = getUrlButton();
489 	const auto url = button ? QString::fromUtf8(button->data) : QString();
490 	const auto text = _fullDisplayed ? QString() : buttonText();
491 	if (!url.isEmpty() && !text.isEmpty()) {
492 		return QString("%1\n\n%2").arg(text, url);
493 	} else if (url.isEmpty() != text.isEmpty()) {
494 		return text + url;
495 	} else {
496 		return QString();
497 	}
498 }
499 
500 ReplyKeyboard::Button::Button() = default;
501 ReplyKeyboard::Button::Button(Button &&other) = default;
502 ReplyKeyboard::Button &ReplyKeyboard::Button::operator=(
503 	Button &&other) = default;
504 ReplyKeyboard::Button::~Button() = default;
505 
ReplyKeyboard(not_null<const HistoryItem * > item,std::unique_ptr<Style> && s)506 ReplyKeyboard::ReplyKeyboard(
507 	not_null<const HistoryItem*> item,
508 	std::unique_ptr<Style> &&s)
509 : _item(item)
510 , _selectedAnimation([=](crl::time now) {
511 	return selectedAnimationCallback(now);
512 })
513 , _st(std::move(s)) {
514 	if (const auto markup = _item->Get<HistoryMessageReplyMarkup>()) {
515 		const auto owner = &_item->history()->owner();
516 		const auto context = _item->fullId();
517 		const auto rowCount = int(markup->data.rows.size());
518 		_rows.reserve(rowCount);
519 		for (auto i = 0; i != rowCount; ++i) {
520 			const auto &row = markup->data.rows[i];
521 			const auto rowSize = int(row.size());
522 			auto newRow = std::vector<Button>();
523 			newRow.reserve(rowSize);
524 			for (auto j = 0; j != rowSize; ++j) {
525 				auto button = Button();
526 				const auto text = row[j].text;
527 				button.type = row.at(j).type;
528 				button.link = std::make_shared<ReplyMarkupClickHandler>(
529 					owner,
530 					i,
531 					j,
532 					context);
533 				button.text.setText(
534 					_st->textStyle(),
535 					TextUtilities::SingleLine(text),
536 					_textPlainOptions);
537 				button.characters = text.isEmpty() ? 1 : text.size();
538 				newRow.push_back(std::move(button));
539 			}
540 			_rows.push_back(std::move(newRow));
541 		}
542 	}
543 }
544 
updateMessageId()545 void ReplyKeyboard::updateMessageId() {
546 	const auto msgId = _item->fullId();
547 	for (const auto &row : _rows) {
548 		for (const auto &button : row) {
549 			button.link->setMessageId(msgId);
550 		}
551 	}
552 
553 }
554 
resize(int width,int height)555 void ReplyKeyboard::resize(int width, int height) {
556 	_width = width;
557 
558 	auto y = 0.;
559 	auto buttonHeight = _rows.empty()
560 		? float64(_st->buttonHeight())
561 		: (float64(height + _st->buttonSkip()) / _rows.size());
562 	for (auto &row : _rows) {
563 		int s = row.size();
564 
565 		int widthForButtons = _width - ((s - 1) * _st->buttonSkip());
566 		int widthForText = widthForButtons;
567 		int widthOfText = 0;
568 		int maxMinButtonWidth = 0;
569 		for (const auto &button : row) {
570 			widthOfText += qMax(button.text.maxWidth(), 1);
571 			int minButtonWidth = _st->minButtonWidth(button.type);
572 			widthForText -= minButtonWidth;
573 			accumulate_max(maxMinButtonWidth, minButtonWidth);
574 		}
575 		bool exact = (widthForText == widthOfText);
576 		bool enough = (widthForButtons - s * maxMinButtonWidth) >= widthOfText;
577 
578 		float64 x = 0;
579 		for (auto &button : row) {
580 			int buttonw = qMax(button.text.maxWidth(), 1);
581 			float64 textw = buttonw, minw = _st->minButtonWidth(button.type);
582 			float64 w = textw;
583 			if (exact) {
584 				w += minw;
585 			} else if (enough) {
586 				w = (widthForButtons / float64(s));
587 				textw = w - minw;
588 			} else {
589 				textw = (widthForText / float64(s));
590 				w = minw + textw;
591 				accumulate_max(w, 2 * float64(_st->buttonPadding()));
592 			}
593 
594 			int rectx = static_cast<int>(std::floor(x));
595 			int rectw = static_cast<int>(std::floor(x + w)) - rectx;
596 			button.rect = QRect(rectx, qRound(y), rectw, qRound(buttonHeight - _st->buttonSkip()));
597 			if (rtl()) button.rect.setX(_width - button.rect.x() - button.rect.width());
598 			x += w + _st->buttonSkip();
599 
600 			button.link->setFullDisplayed(textw >= buttonw);
601 		}
602 		y += buttonHeight;
603 	}
604 }
605 
isEnoughSpace(int width,const style::BotKeyboardButton & st) const606 bool ReplyKeyboard::isEnoughSpace(int width, const style::BotKeyboardButton &st) const {
607 	for (const auto &row : _rows) {
608 		int s = row.size();
609 		int widthLeft = width - ((s - 1) * st.margin + s * 2 * st.padding);
610 		for (const auto &button : row) {
611 			widthLeft -= qMax(button.text.maxWidth(), 1);
612 			if (widthLeft < 0) {
613 				if (row.size() > 3) {
614 					return false;
615 				} else {
616 					break;
617 				}
618 			}
619 		}
620 	}
621 	return true;
622 }
623 
setStyle(std::unique_ptr<Style> && st)624 void ReplyKeyboard::setStyle(std::unique_ptr<Style> &&st) {
625 	_st = std::move(st);
626 }
627 
naturalWidth() const628 int ReplyKeyboard::naturalWidth() const {
629 	auto result = 0;
630 	for (const auto &row : _rows) {
631 		auto maxMinButtonWidth = 0;
632 		for (const auto &button : row) {
633 			accumulate_max(
634 				maxMinButtonWidth,
635 				_st->minButtonWidth(button.type));
636 		}
637 		auto rowMaxButtonWidth = 0;
638 		for (const auto &button : row) {
639 			accumulate_max(
640 				rowMaxButtonWidth,
641 				qMax(button.text.maxWidth(), 1) + maxMinButtonWidth);
642 		}
643 
644 		const auto rowSize = int(row.size());
645 		accumulate_max(
646 			result,
647 			rowSize * rowMaxButtonWidth + (rowSize - 1) * _st->buttonSkip());
648 	}
649 	return result;
650 }
651 
naturalHeight() const652 int ReplyKeyboard::naturalHeight() const {
653 	return (_rows.size() - 1) * _st->buttonSkip() + _rows.size() * _st->buttonHeight();
654 }
655 
paint(Painter & p,const Ui::ChatStyle * st,int outerWidth,const QRect & clip) const656 void ReplyKeyboard::paint(
657 		Painter &p,
658 		const Ui::ChatStyle *st,
659 		int outerWidth,
660 		const QRect &clip) const {
661 	Assert(_st != nullptr);
662 	Assert(_width > 0);
663 
664 	_st->startPaint(p, st);
665 	for (const auto &row : _rows) {
666 		for (const auto &button : row) {
667 			const auto rect = button.rect;
668 			if (rect.y() >= clip.y() + clip.height()) return;
669 			if (rect.y() + rect.height() < clip.y()) continue;
670 
671 			// just ignore the buttons that didn't layout well
672 			if (rect.x() + rect.width() > _width) break;
673 
674 			_st->paintButton(p, st, outerWidth, button);
675 		}
676 	}
677 }
678 
getLink(QPoint point) const679 ClickHandlerPtr ReplyKeyboard::getLink(QPoint point) const {
680 	Assert(_width > 0);
681 
682 	for (const auto &row : _rows) {
683 		for (const auto &button : row) {
684 			QRect rect(button.rect);
685 
686 			// just ignore the buttons that didn't layout well
687 			if (rect.x() + rect.width() > _width) break;
688 
689 			if (rect.contains(point)) {
690 				_savedCoords = point;
691 				return button.link;
692 			}
693 		}
694 	}
695 	return ClickHandlerPtr();
696 }
697 
clickHandlerActiveChanged(const ClickHandlerPtr & p,bool active)698 void ReplyKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
699 	if (!p) return;
700 
701 	_savedActive = active ? p : ClickHandlerPtr();
702 	auto coords = findButtonCoordsByClickHandler(p);
703 	if (coords.i >= 0 && _savedPressed != p) {
704 		startAnimation(coords.i, coords.j, active ? 1 : -1);
705 	}
706 }
707 
findButtonCoordsByClickHandler(const ClickHandlerPtr & p)708 ReplyKeyboard::ButtonCoords ReplyKeyboard::findButtonCoordsByClickHandler(const ClickHandlerPtr &p) {
709 	for (int i = 0, rows = _rows.size(); i != rows; ++i) {
710 		auto &row = _rows[i];
711 		for (int j = 0, cols = row.size(); j != cols; ++j) {
712 			if (row[j].link == p) {
713 				return { i, j };
714 			}
715 		}
716 	}
717 	return { -1, -1 };
718 }
719 
clickHandlerPressedChanged(const ClickHandlerPtr & handler,bool pressed)720 void ReplyKeyboard::clickHandlerPressedChanged(
721 		const ClickHandlerPtr &handler,
722 		bool pressed) {
723 	if (!handler) return;
724 
725 	_savedPressed = pressed ? handler : ClickHandlerPtr();
726 	auto coords = findButtonCoordsByClickHandler(handler);
727 	if (coords.i >= 0) {
728 		auto &button = _rows[coords.i][coords.j];
729 		if (pressed) {
730 			if (!button.ripple) {
731 				auto mask = Ui::RippleAnimation::roundRectMask(
732 					button.rect.size(),
733 					_st->buttonRadius());
734 				button.ripple = std::make_unique<Ui::RippleAnimation>(
735 					_st->_st->ripple,
736 					std::move(mask),
737 					[this] { _st->repaint(_item); });
738 			}
739 			button.ripple->add(_savedCoords - button.rect.topLeft());
740 		} else {
741 			if (button.ripple) {
742 				button.ripple->lastStop();
743 			}
744 			if (_savedActive != handler) {
745 				startAnimation(coords.i, coords.j, -1);
746 			}
747 		}
748 	}
749 }
750 
startAnimation(int i,int j,int direction)751 void ReplyKeyboard::startAnimation(int i, int j, int direction) {
752 	auto notStarted = _animations.empty();
753 
754 	int indexForAnimation = Layout::PositionToIndex(i, j + 1) * direction;
755 
756 	_animations.remove(-indexForAnimation);
757 	if (!_animations.contains(indexForAnimation)) {
758 		_animations.emplace(indexForAnimation, crl::now());
759 	}
760 
761 	if (notStarted && !_selectedAnimation.animating()) {
762 		_selectedAnimation.start();
763 	}
764 }
765 
selectedAnimationCallback(crl::time now)766 bool ReplyKeyboard::selectedAnimationCallback(crl::time now) {
767 	if (anim::Disabled()) {
768 		now += st::botKbDuration;
769 	}
770 	for (auto i = _animations.begin(); i != _animations.end();) {
771 		const auto index = std::abs(i->first) - 1;
772 		const auto &[row, col] = Layout::IndexToPosition(index);
773 		const auto dt = float64(now - i->second) / st::botKbDuration;
774 		if (dt >= 1) {
775 			_rows[row][col].howMuchOver = (i->first > 0) ? 1 : 0;
776 			i = _animations.erase(i);
777 		} else {
778 			_rows[row][col].howMuchOver = (i->first > 0) ? dt : (1 - dt);
779 			++i;
780 		}
781 	}
782 	_st->repaint(_item);
783 	return !_animations.empty();
784 }
785 
clearSelection()786 void ReplyKeyboard::clearSelection() {
787 	for (const auto &[relativeIndex, time] : _animations) {
788 		const auto index = std::abs(relativeIndex) - 1;
789 		const auto &[row, col] = Layout::IndexToPosition(index);
790 		_rows[row][col].howMuchOver = 0;
791 	}
792 	_animations.clear();
793 	_selectedAnimation.stop();
794 }
795 
buttonSkip() const796 int ReplyKeyboard::Style::buttonSkip() const {
797 	return _st->margin;
798 }
799 
buttonPadding() const800 int ReplyKeyboard::Style::buttonPadding() const {
801 	return _st->padding;
802 }
803 
buttonHeight() const804 int ReplyKeyboard::Style::buttonHeight() const {
805 	return _st->height;
806 }
807 
paintButton(Painter & p,const Ui::ChatStyle * st,int outerWidth,const ReplyKeyboard::Button & button) const808 void ReplyKeyboard::Style::paintButton(
809 		Painter &p,
810 		const Ui::ChatStyle *st,
811 		int outerWidth,
812 		const ReplyKeyboard::Button &button) const {
813 	const QRect &rect = button.rect;
814 	paintButtonBg(p, st, rect, button.howMuchOver);
815 	if (button.ripple) {
816 		const auto color = st ? &st->msgBotKbRippleBg()->c : nullptr;
817 		button.ripple->paint(p, rect.x(), rect.y(), outerWidth, color);
818 		if (button.ripple->empty()) {
819 			button.ripple.reset();
820 		}
821 	}
822 	paintButtonIcon(p, st, rect, outerWidth, button.type);
823 	if (button.type == HistoryMessageMarkupButton::Type::CallbackWithPassword
824 		|| button.type == HistoryMessageMarkupButton::Type::Callback
825 		|| button.type == HistoryMessageMarkupButton::Type::Game) {
826 		if (const auto data = button.link->getButton()) {
827 			if (data->requestId) {
828 				paintButtonLoading(p, st, rect);
829 			}
830 		}
831 	}
832 
833 	int tx = rect.x(), tw = rect.width();
834 	if (tw >= st::botKbStyle.font->elidew + _st->padding * 2) {
835 		tx += _st->padding;
836 		tw -= _st->padding * 2;
837 	} else if (tw > st::botKbStyle.font->elidew) {
838 		tx += (tw - st::botKbStyle.font->elidew) / 2;
839 		tw = st::botKbStyle.font->elidew;
840 	}
841 	button.text.drawElided(p, tx, rect.y() + _st->textTop + ((rect.height() - _st->height) / 2), tw, 1, style::al_top);
842 }
843 
createForwarded(const HistoryMessageReplyMarkup & original)844 void HistoryMessageReplyMarkup::createForwarded(
845 		const HistoryMessageReplyMarkup &original) {
846 	Expects(!inlineKeyboard);
847 
848 	data.fillForwardedData(original.data);
849 }
850 
updateData(HistoryMessageMarkupData && markup)851 void HistoryMessageReplyMarkup::updateData(
852 		HistoryMessageMarkupData &&markup) {
853 	data = std::move(markup);
854 	inlineKeyboard = nullptr;
855 }
856 
857 HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal() = default;
858 
HistoryMessageLogEntryOriginal(HistoryMessageLogEntryOriginal && other)859 HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal(
860 	HistoryMessageLogEntryOriginal &&other)
861 : page(std::move(other.page)) {
862 }
863 
operator =(HistoryMessageLogEntryOriginal && other)864 HistoryMessageLogEntryOriginal &HistoryMessageLogEntryOriginal::operator=(
865 		HistoryMessageLogEntryOriginal &&other) {
866 	page = std::move(other.page);
867 	return *this;
868 }
869 
870 HistoryMessageLogEntryOriginal::~HistoryMessageLogEntryOriginal() = default;
871 
HistoryDocumentCaptioned()872 HistoryDocumentCaptioned::HistoryDocumentCaptioned()
873 : _caption(st::msgFileMinWidth - st::msgPadding.left() - st::msgPadding.right()) {
874 }
875 
HistoryDocumentVoicePlayback(const HistoryView::Document * that)876 HistoryDocumentVoicePlayback::HistoryDocumentVoicePlayback(
877 	const HistoryView::Document *that)
878 : progress(0., 0.)
879 , progressAnimation([=](crl::time now) {
880 	const auto nonconst = const_cast<HistoryView::Document*>(that);
881 	return nonconst->voiceProgressAnimationCallback(now);
882 }) {
883 }
884 
ensurePlayback(const HistoryView::Document * that) const885 void HistoryDocumentVoice::ensurePlayback(
886 		const HistoryView::Document *that) const {
887 	if (!_playback) {
888 		_playback = std::make_unique<HistoryDocumentVoicePlayback>(that);
889 	}
890 }
891 
checkPlaybackFinished() const892 void HistoryDocumentVoice::checkPlaybackFinished() const {
893 	if (_playback && !_playback->progressAnimation.animating()) {
894 		_playback.reset();
895 	}
896 }
897 
startSeeking()898 void HistoryDocumentVoice::startSeeking() {
899 	_seeking = true;
900 	_seekingCurrent = _seekingStart;
901 	Media::Player::instance()->startSeeking(AudioMsgId::Type::Voice);
902 }
903 
stopSeeking()904 void HistoryDocumentVoice::stopSeeking() {
905 	_seeking = false;
906 	Media::Player::instance()->cancelSeeking(AudioMsgId::Type::Voice);
907 }
908