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