/* 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 "dialogs/ui/dialogs_top_bar_suggestion_content.h" #include "base/call_delayed.h" #include "data/data_authorization.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "settings/settings_common.h" #include "ui/effects/animation_value.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/rect.h" #include "ui/text/format_values.h" #include "ui/text/text_custom_emoji.h" #include "ui/ui_rpl_filter.h" #include "ui/ui_utility.h" #include "ui/vertical_list.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/shadow.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_chat.h" #include "styles/style_dialogs.h" #include "styles/style_layers.h" #include "styles/style_premium.h" #include "styles/style_settings.h" namespace Dialogs { namespace { [[nodiscard]] int PillRadius() { const auto padding = st::msgReplyPadding.top(); return (padding + st::semiboldTextStyle.font->height + st::dialogsTopBarSuggestionAboutStyle.font->height + padding) / 2; } void PaintTopFade(QPainter &p, int outerWidth, int fadeHeight) { if (fadeHeight <= 0) { return; } auto transparent = st::dialogsBg->c; transparent.setAlpha(0); auto grad = QLinearGradient(0, 0, 0, fadeHeight); grad.setColorAt(0, st::dialogsBg->c); grad.setColorAt(1, transparent); p.fillRect(QRect(0, 0, outerWidth, fadeHeight), grad); } [[nodiscard]] QString FormatUnconfirmedAuthMessage( const std::vector &list) { if (list.empty()) { return QString(); } else if (list.size() == 1) { const auto &auth = list.at(0); return tr::lng_unconfirmed_auth_single( tr::now, lt_from, auth.device, lt_country, auth.location); } auto commonLocation = list.at(0).location; for (auto i = 1; i < list.size(); ++i) { if (commonLocation != list.at(i).location) { commonLocation.clear(); break; } } if (commonLocation.isEmpty()) { return tr::lng_unconfirmed_auth_multiple( tr::now, lt_count, list.size()); } return tr::lng_unconfirmed_auth_multiple_from( tr::now, lt_count, list.size(), lt_country, commonLocation); } void PaintPillTopSheen(QPainter &p, const QRect &pill, int radius) { if (pill.isEmpty() || st::dialogsBg->c.lightness() >= 128) { return; } auto top = QColor(255, 255, 255, 40); auto mid = QColor(255, 255, 255, 0); auto bottom = QColor(255, 255, 255, 20); auto grad = QLinearGradient(0, pill.top(), 0, pill.bottom()); grad.setColorAt(0., top); grad.setColorAt(0.5, mid); grad.setColorAt(1., bottom); p.setPen(QPen(QBrush(grad), st::lineWidth)); p.setBrush(Qt::NoBrush); const auto half = 0.5 * st::lineWidth; const auto stroke = QRectF(pill).adjusted(half, half, -half, -half); p.drawRoundedRect(stroke, radius - half, radius - half); } } // namespace UnconfirmedAuthWrap::UnconfirmedAuthWrap( not_null parent, object_ptr &&child) : Ui::SlideWrap(parent, std::move(child)) , _shadow(st::dialogsTopBarSuggestionShadow) { paintRequest() | rpl::on_next([=] { if (!_collapseSnapshot.isNull()) { auto p = QPainter(this); p.drawPixmap(0, 0, _collapseSnapshot); } }, lifetime()); } rpl::producer UnconfirmedAuthWrap::desiredHeightValue() const { return entity()->heightValue(); } void UnconfirmedAuthWrap::setCollapseProgress( rpl::producer progress) { std::move(progress) | rpl::on_next([=](float64 value) { if (_collapseProgress == value) { return; } _collapseProgress = value; if (value == 0.) { releaseCollapseSnapshot(); } resizeToWidth(width()); update(); }, lifetime()); } void UnconfirmedAuthWrap::prepareCollapseSnapshot() { _collapseSnapshot = Ui::GrabWidget(this); if (const auto w = wrapped()) { w->hide(); } update(); } void UnconfirmedAuthWrap::releaseCollapseSnapshot() { if (_collapseSnapshot.isNull()) { return; } _collapseSnapshot = QPixmap(); if (const auto w = wrapped()) { w->show(); } } int UnconfirmedAuthWrap::resizeGetHeight(int newWidth) { if (!_collapseSnapshot.isNull()) { const auto fullHeight = int(_collapseSnapshot.height() / _collapseSnapshot.devicePixelRatio()); return int(base::SafeRound( fullHeight * (1. - _collapseProgress))); } if (const auto w = wrapped()) { w->resizeToWidth(newWidth); } return wrapped() ? wrapped()->height() : 0; } not_null CreateUnconfirmedAuthContent( not_null parent, rpl::producer> list, Fn callback, rpl::producer collapseProgress) { const auto wrap = Ui::CreateChild( parent, object_ptr(parent)); wrap->setCollapseProgress(std::move(collapseProgress)); const auto content = wrap->entity(); const auto &margins = st::dialogsTopBarSuggestionMargins; content->paintOn([=](QPainter &p) { const auto outer = content->rect(); const auto pill = outer - margins; PaintTopFade(p, outer.width(), margins.top() + pill.height() / 2); if (pill.isEmpty()) { return; } const auto radius = std::min({ PillRadius(), pill.width() / 2, pill.height() / 2, }); wrap->shadow().paint(p, pill, radius); auto hq = PainterHighQualityEnabler(p); p.setBrush(st::dialogsBg); p.setPen(Qt::NoPen); p.drawRoundedRect(pill, radius, radius); PaintPillTopSheen(p, pill, radius); }); const auto &basePadding = st::dialogsUnconfirmedAuthPadding; const auto padding = QMargins( margins.left() + basePadding.left(), basePadding.top(), margins.right() + basePadding.right(), basePadding.bottom()); Ui::AddSkip(content, margins.top()); Ui::AddSkip(content); content->add( object_ptr( content, tr::lng_unconfirmed_auth_title(), st::dialogsUnconfirmedAuthTitle), padding, style::al_top); Ui::AddSkip(content); content->add( object_ptr( content, std::move(list) | rpl::map(FormatUnconfirmedAuthMessage), st::dialogsUnconfirmedAuthAbout), padding, style::al_top)->setTryMakeSimilarLines(true); Ui::AddSkip(content); const auto buttons = content->add(object_ptr( content, st::dialogsUnconfirmedAuthButton.height)); const auto yes = Ui::CreateChild( buttons, tr::lng_unconfirmed_auth_confirm(), st::dialogsUnconfirmedAuthButton); const auto no = Ui::CreateChild( buttons, tr::lng_unconfirmed_auth_deny(), st::dialogsUnconfirmedAuthButtonNo); yes->setClickedCallback([=] { wrap->toggle(false, anim::type::normal); base::call_delayed(st::universalDuration, wrap, [=] { callback(true); }); }); no->setClickedCallback([=] { wrap->toggle(false, anim::type::normal); base::call_delayed(st::universalDuration, wrap, [=] { callback(false); }); }); buttons->sizeValue( ) | rpl::filter_size( ) | rpl::on_next([=](const QSize &s) { const auto halfWidth = (s.width() - rect::m::sum::h(padding)) / 2; yes->moveToLeft( padding.left() + (halfWidth - yes->width()) / 2, 0); no->moveToLeft( padding.left() + halfWidth + (halfWidth - no->width()) / 2, 0); }, buttons->lifetime()); Ui::AddSkip(content); Ui::AddSkip(content, margins.bottom()); return wrap; } TopBarSuggestionContent::TopBarSuggestionContent( not_null parent, Fn emojiPaused) : Ui::RippleButton(parent, st::defaultRippleAnimationBgOver) , _titleSt(st::semiboldTextStyle) , _contentTitleSt(st::dialogsTopBarSuggestionTitleStyle) , _contentTextSt(st::dialogsTopBarSuggestionAboutStyle) , _shadow(st::dialogsTopBarSuggestionShadow) , _emojiPaused(std::move(emojiPaused)) { _leftPadding = st::dialogsTopBarLeftPadding; setRightIcon(RightIcon::Close); } void TopBarSuggestionContent::setRightIcon(RightIcon icon) { _rightButton = nullptr; if (icon == _rightIcon) { return; } _rightHide = nullptr; _rightArrow = nullptr; _rightIcon = icon; const auto &margins = st::dialogsTopBarSuggestionMargins; if (icon == RightIcon::Close) { _rightHide = base::make_unique_q( this, st::dialogsCancelSearchInPeer); const auto rightHide = _rightHide.get(); sizeValue() | rpl::filter_size( ) | rpl::on_next([=](const QSize &s) { const auto &button = st::dialogsCancelSearchInPeer; const auto padding = PillRadius() - button.rippleAreaSize / 2; const auto pillHeight = s.height() - rect::m::sum::v(margins); rightHide->moveToRight( margins.right() + padding - button.rippleAreaPosition.x(), margins.top() + (pillHeight - button.rippleAreaSize) / 2 - button.rippleAreaPosition.y()); }, rightHide->lifetime()); rightHide->show(); } else if (icon == RightIcon::Arrow) { _rightArrow = base::make_unique_q( this, st::backButton); const auto arrow = _rightArrow.get(); arrow->setIconOverride( &st::settingsPremiumArrow, &st::settingsPremiumArrowOver); arrow->setAttribute(Qt::WA_TransparentForMouseEvents); sizeValue() | rpl::filter_size( ) | rpl::on_next([=](const QSize &s) { const auto &point = st::settingsPremiumArrowShift; const auto pillRight = s.width() - margins.right(); const auto pillHeight = s.height() - rect::m::sum::v(margins); arrow->moveToLeft( pillRight - arrow->width(), margins.top() + point.y() + (pillHeight - arrow->height()) / 2); }, arrow->lifetime()); arrow->show(); } resizeToWidth(width()); } void TopBarSuggestionContent::setRightButton( rpl::producer text, Fn callback) { _rightHide = nullptr; _rightArrow = nullptr; _rightIcon = RightIcon::None; if (!text) { _rightButton = nullptr; return; } using namespace Ui; _rightButton = base::make_unique_q( this, rpl::single(QString()), st::dialogsTopBarRightButton); _rightButton->setText(std::move(text)); const auto &margins = st::dialogsTopBarSuggestionMargins; rpl::combine( sizeValue(), _rightButton->sizeValue() ) | rpl::on_next([=](QSize outer, QSize inner) { const auto pillHeight = outer.height() - rect::m::sum::v(margins); const auto verticalGap = (pillHeight - inner.height()) / 2; _rightButton->moveToRight( margins.right() + verticalGap, margins.top() + verticalGap, outer.width()); }, _rightButton->lifetime()); _rightButton->setFullRadius(true); _rightButton->setClickedCallback(std::move(callback)); _rightButton->show(); } void TopBarSuggestionContent::draw(QPainter &p) { const auto outer = Ui::RpWidget::rect(); const auto &margins = st::dialogsTopBarSuggestionMargins; const auto pill = outer - margins; PaintTopFade(p, outer.width(), margins.top() + pill.height() / 2); if (pill.isEmpty()) { return; } const auto radius = std::min({ PillRadius(), pill.width() / 2, pill.height() / 2, }); _shadow.paint(p, pill, radius); auto hq = PainterHighQualityEnabler(p); p.setBrush(st::dialogsBg); p.setPen(Qt::NoPen); p.drawRoundedRect(pill, radius, radius); PaintPillTopSheen(p, pill, radius); auto clipPath = QPainterPath(); clipPath.addRoundedRect(pill, radius, radius); p.setClipPath(clipPath); Ui::RippleButton::paintRipple(p, 0, 0); p.setClipping(false); const auto leftPadding = _leftPadding + margins.left(); const auto rightPadding = margins.right(); const auto topPadding = st::msgReplyPadding.top() + margins.top(); const auto availableWidthNoPhoto = outer.width() - (_rightArrow ? (_rightArrow->width() / 4 * 3) // Takes full height. : 0) - leftPadding - rightPadding; const auto availableWidth = availableWidthNoPhoto - (_rightHide ? _rightHide->width() : 0); const auto titleRight = leftPadding; const auto hasSecondLineTitle = availableWidth < _contentTitle.maxWidth(); const auto paused = On(PowerSaving::kEmojiChat) || (_emojiPaused && _emojiPaused()); p.setPen(st::windowActiveTextFg); p.setPen(st::windowFg); { const auto left = leftPadding; const auto top = topPadding; _contentTitle.draw(p, { .position = QPoint(left, top), .outerWidth = hasSecondLineTitle ? availableWidth : (availableWidth - titleRight), .availableWidth = availableWidth, .pausedEmoji = paused, .elisionLines = hasSecondLineTitle ? 2 : 1, }); } { const auto left = leftPadding; const auto top = hasSecondLineTitle ? (topPadding + _titleSt.font->height + _contentTitleSt.font->height) : topPadding + _titleSt.font->height; const auto lineHeight = _contentTextSt.font->height; const auto lineLayout = [=](int line) -> Ui::Text::LineGeometry { line++; const auto diff = (st::sponsoredMessageBarMaxHeight) - line * lineHeight; if (diff < 3 * lineHeight) { return { .width = availableWidth, .elided = true, }; } else if (diff < 2 * lineHeight) { return {}; } return { .width = availableWidth }; }; p.setPen(_descriptionColorOverride.value_or(st::windowSubTextFg->c)); _contentText.draw(p, { .position = QPoint(left, top), .outerWidth = availableWidth, .availableWidth = availableWidth, .geometry = Ui::Text::GeometryDescriptor{ .layout = std::move(lineLayout), }, .pausedEmoji = paused, }); } } void TopBarSuggestionContent::setContent( TextWithEntities title, TextWithEntities description, std::optional context, std::optional descriptionColorOverride) { _descriptionColorOverride = descriptionColorOverride; if (context) { context->repaint = [=] { update(); }; _contentTitle.setMarkedText( _contentTitleSt, std::move(title), kMarkupTextOptions, *context); _contentText.setMarkedText( _contentTextSt, std::move(description), kMarkupTextOptions, base::take(*context)); } else { _contentTitle.setMarkedText(_contentTitleSt, std::move(title)); _contentText.setMarkedText(_contentTextSt, std::move(description)); } resizeToWidth(width()); update(); } void TopBarSuggestionContent::paintEvent(QPaintEvent *) { auto p = QPainter(this); if (!_collapseSnapshot.isNull()) { p.drawPixmap(0, 0, _collapseSnapshot); return; } draw(p); } void TopBarSuggestionContent::prepareCollapseSnapshot() { _collapseSnapshot = Ui::GrabWidget(this); for (const auto child : children()) { if (const auto widget = qobject_cast(child)) { widget->hide(); } } update(); } void TopBarSuggestionContent::releaseCollapseSnapshot() { if (_collapseSnapshot.isNull()) { return; } _collapseSnapshot = QPixmap(); for (const auto child : children()) { if (const auto widget = qobject_cast(child)) { widget->show(); } } } int TopBarSuggestionContent::resizeGetHeight(int newWidth) { if (!_collapseSnapshot.isNull()) { const auto fullHeight = int(_collapseSnapshot.height() / _collapseSnapshot.devicePixelRatio()); return int(base::SafeRound( fullHeight * (1. - _collapseProgress))); } const auto &margins = st::dialogsTopBarSuggestionMargins; const auto topPadding = st::msgReplyPadding.top(); const auto bottomPadding = st::msgReplyPadding.top(); const auto availableWidthNoPhoto = newWidth - rect::m::sum::h(margins) - (_rightArrow ? (_rightArrow->width() / 4 * 3) // Takes full height. : 0) - _leftPadding; const auto availableWidth = availableWidthNoPhoto - (_rightHide ? _rightHide->width() : 0); if (availableWidth <= 0) { return topPadding + bottomPadding + rect::m::sum::v(margins); } const auto hasSecondLineTitle = (availableWidth < _contentTitle.maxWidth()); const auto textTop = hasSecondLineTitle ? (topPadding + _titleSt.font->height + _contentTitleSt.font->height) : (topPadding + _titleSt.font->height); const auto lineHeight = _contentTextSt.font->height; auto lineLayout = [=](int line) -> Ui::Text::LineGeometry { line++; const auto diff = (st::sponsoredMessageBarMaxHeight) - line * lineHeight; if (diff < 3 * lineHeight) { return { .width = availableWidth, .elided = true, }; } else if (diff < 2 * lineHeight) { return {}; } return { .width = availableWidth }; }; const auto dims = _contentText.countDimensions( Ui::Text::GeometryDescriptor{ .layout = std::move(lineLayout) }); const auto natural = textTop + dims.height + bottomPadding; const auto capped = std::min( natural, st::sponsoredMessageBarMaxHeight); const auto withMargins = capped + rect::m::sum::v(margins); return int(base::SafeRound(withMargins * (1. - _collapseProgress))); } void TopBarSuggestionContent::setCollapseProgress( rpl::producer progress) { std::move(progress) | rpl::on_next([=](float64 value) { if (_collapseProgress == value) { return; } _collapseProgress = value; if (value == 0.) { releaseCollapseSnapshot(); } resizeToWidth(width()); update(); }, lifetime()); } void TopBarSuggestionContent::setHideCallback(Fn hideCallback) { Expects(_rightHide != nullptr); _rightHide->setClickedCallback(std::move(hideCallback)); } void TopBarSuggestionContent::setLeadingWidget(Ui::RpWidget *widget) { _leadingWidgetLifetime.destroy(); if (_leadingWidget && _leadingWidget != widget) { _leadingWidget->deleteLater(); } _leadingWidget = widget; const auto basePadding = st::dialogsTopBarLeftPadding; if (!widget) { if (_leftPadding != basePadding) { _leftPadding = basePadding; resizeToWidth(width()); update(); } return; } widget->setParent(this); widget->setAttribute(Qt::WA_TransparentForMouseEvents); const auto &margins = st::dialogsTopBarSuggestionMargins; const auto &row = st::defaultDialogRow; sizeValue() | rpl::filter_size( ) | rpl::on_next([=](const QSize &s) { widget->raise(); widget->show(); const auto pillHeight = s.height() - rect::m::sum::v(margins); widget->moveToLeft( row.padding.left() + (row.photoSize - widget->width()) / 2, margins.top() + (pillHeight - widget->height()) / 2); }, _leadingWidgetLifetime); const auto padding = row.nameLeft - margins.left(); if (_leftPadding != padding) { _leftPadding = padding; resizeToWidth(width()); update(); } } const style::TextStyle & TopBarSuggestionContent::contentTitleSt() const { return _contentTitleSt; } } // namespace Dialogs