From 1e89ee4e50cddef1d8e78e2216de105182192f73 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 28 Nov 2025 17:28:27 +0400 Subject: [PATCH] Show Reject/Accept buttons for offers. --- Telegram/Resources/langs/lang.strings | 4 +- Telegram/SourceFiles/api/api_suggest_post.cpp | 1 + Telegram/SourceFiles/core/credits_amount.h | 2 + .../SourceFiles/data/components/credits.cpp | 13 + .../SourceFiles/data/data_media_types.cpp | 5 +- Telegram/SourceFiles/data/data_media_types.h | 1 + Telegram/SourceFiles/history/history_item.cpp | 163 +++++++++- Telegram/SourceFiles/history/history_item.h | 3 +- .../history/history_item_components.cpp | 19 +- .../history/history_item_components.h | 6 +- .../controls/history_view_suggest_options.cpp | 2 +- .../history/view/history_view_element.cpp | 302 ++++++++++++++++++ .../history/view/history_view_element.h | 2 + .../history/view/history_view_message.cpp | 301 +---------------- .../history/view/history_view_message.h | 1 - .../view/history_view_service_message.cpp | 87 ++++- .../view/history_view_service_message.h | 4 + .../view/media/history_view_service_box.cpp | 43 +++ .../view/media/history_view_service_box.h | 12 +- .../media/history_view_theme_document.cpp | 162 ++++++++-- .../view/media/history_view_theme_document.h | 17 +- 21 files changed, 796 insertions(+), 354 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 608b536586..7ec20df0ac 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2351,6 +2351,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_gift_offer" = "{user} offered you {cost} for {name}."; "lng_action_gift_offer_you" = "You offered {cost} for {name}."; "lng_action_gift_offer_state_expires" = "This offer expires in {time}."; +"lng_action_gift_offer_time_medium" = "{hours} h {minutes} m"; +"lng_action_gift_offer_time_small" = "{minutes} m"; "lng_action_gift_offer_state_accepted" = "This offer was accepted."; "lng_action_gift_offer_state_rejected" = "This offer was rejected."; "lng_action_gift_offer_state_expired" = "This offer has expired."; @@ -2359,7 +2361,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_gift_offer_decline" = "Reject"; "lng_action_gift_offer_accept" = "Accept"; "lng_action_gift_offer_expired" = "The offer from {user} to buy your {name} for {cost} has expired."; -"lng_action_gift_offer_expired_you" = "Your offer to buy {name} for {cost} has expired."; +"lng_action_gift_offer_expired_your" = "Your offer to buy {name} for {cost} has expired."; "lng_action_gift_offer_declined" = "{user} rejected your offer to buy {name} for {cost}."; "lng_action_gift_offer_declined_you" = "You rejected {user}'s offer to buy your {name} for {cost}."; "lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile."; diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index 64be6aae26..f973c4fcaa 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -546,6 +546,7 @@ std::shared_ptr DeclineClickHandler( if (!item) { return; } + RequestDeclineComment(controller->uiShow(), item); }); } diff --git a/Telegram/SourceFiles/core/credits_amount.h b/Telegram/SourceFiles/core/credits_amount.h index 098266d887..42bf1a522d 100644 --- a/Telegram/SourceFiles/core/credits_amount.h +++ b/Telegram/SourceFiles/core/credits_amount.h @@ -181,3 +181,5 @@ private: [[nodiscard]] CreditsAmount CreditsAmountFromTL( const MTPStarsAmount *amount); [[nodiscard]] MTPStarsAmount StarsAmountToTL(CreditsAmount amount); + +[[nodiscard]] QString PrepareCreditsAmountText(CreditsAmount amount); diff --git a/Telegram/SourceFiles/data/components/credits.cpp b/Telegram/SourceFiles/data/components/credits.cpp index 0bca68827f..121dad2da0 100644 --- a/Telegram/SourceFiles/data/components/credits.cpp +++ b/Telegram/SourceFiles/data/components/credits.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "api/api_credits.h" #include "data/data_user.h" +#include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" @@ -256,3 +257,15 @@ MTPStarsAmount StarsAmountToTL(CreditsAmount amount) { MTP_long(amount.whole() * uint64(1'000'000'000) + amount.nano()) ) : MTP_starsAmount(MTP_long(amount.whole()), MTP_int(amount.nano())); } + +QString PrepareCreditsAmountText(CreditsAmount amount) { + return amount.stars() + ? tr::lng_action_gift_for_stars( + tr::now, + lt_count_decimal, + amount.value()) + : tr::lng_action_gift_for_ton( + tr::now, + lt_count_decimal, + amount.value()); +} diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 18c08aa565..7546d047aa 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -2606,10 +2606,11 @@ std::unique_ptr MediaGiftBox::createView( .service = true, .hideServiceText = true, }); - } else if (_data.type == GiftType::ChatTheme) { + } else if (_data.type == GiftType::ChatTheme + || _data.type == GiftType::GiftOffer) { return std::make_unique( message, - std::make_unique(message, this)); + std::make_unique(message, this)); } else if (const auto &unique = _data.unique) { return std::make_unique( message, diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 787e50e14e..78f1357b51 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -140,6 +140,7 @@ enum class GiftType : uchar { StarGift, // count - stars ChatTheme, BirthdaySuggest, + GiftOffer, }; struct GiftCode { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index f28dc5829e..fd42b4ba93 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1098,13 +1098,26 @@ SuggestionActions HistoryItem::computeSuggestionActions() const { SuggestionActions HistoryItem::computeSuggestionActions( const HistoryMessageSuggestion *suggest) const { return suggest - ? computeSuggestionActions(suggest->accepted, suggest->rejected) + ? computeSuggestionActions( + suggest->accepted, + suggest->rejected, + suggest->gift ? suggest->date: 0) : SuggestionActions::None; } SuggestionActions HistoryItem::computeSuggestionActions( bool accepted, - bool rejected) const { + bool rejected, + TimeId giftOfferExpiresAt) const { + if (giftOfferExpiresAt) { + const auto can = isRegular() + && !(accepted || rejected) + && !out() + && (giftOfferExpiresAt > base::unixtime::now()); + return can + ? SuggestionActions::GiftOfferActions + : SuggestionActions::None; + } const auto channelIsAuthor = from()->isChannel(); const auto amMonoforumAdmin = history()->peer->amMonoforumAdmin(); const auto broadcast = history()->peer->monoforumBroadcast(); @@ -4077,7 +4090,8 @@ void HistoryItem::createComponents(CreateConfig &&config) { mask |= HistoryMessageSuggestion::Bit(); if (computeSuggestionActions( config.suggest.accepted, - config.suggest.rejected + config.suggest.rejected, + TimeId() ) != SuggestionActions::None) { mask |= HistoryMessageReplyMarkup::Bit(); } @@ -4827,7 +4841,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { const auto &data = action.c_messageActionSuggestedPostSuccess(); UpdateComponents(HistoryServiceSuggestFinish::Bit()); const auto finish = Get(); - finish->successPrice = CreditsAmountFromTL(data.vprice()); + finish->price = CreditsAmountFromTL(data.vprice()); } else if (type == mtpc_messageActionSuggestedPostRefund) { const auto &data = action.c_messageActionSuggestedPostRefund(); UpdateComponents(HistoryServiceSuggestFinish::Bit()); @@ -4835,6 +4849,43 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { finish->refundType = data.is_payer_initiated() ? SuggestRefundType::User : SuggestRefundType::Admin; + } else if (type == mtpc_messageActionStarGiftPurchaseOffer) { + const auto &data = action.c_messageActionStarGiftPurchaseOffer(); + const auto accepted = data.is_accepted(); + const auto rejected = data.is_declined(); + const auto expiresAt = data.vexpires_at().v; + const auto gift = Api::FromTL(&history()->session(), data.vgift()); + if (const auto unique = gift ? gift->unique : nullptr) { + auto mask = HistoryMessageSuggestion::Bit(); + const auto actions = computeSuggestionActions( + accepted, + rejected, + expiresAt); + if (actions != SuggestionActions::None) { + mask |= HistoryMessageReplyMarkup::Bit(); + } + UpdateComponents(mask); + + const auto suggestion = Get(); + suggestion->price = CreditsAmountFromTL(data.vprice()); + suggestion->date = expiresAt; + suggestion->accepted = accepted; + suggestion->rejected = rejected; + suggestion->gift = unique; + + if (actions != SuggestionActions::None) { + const auto markup = Get(); + markup->updateSuggestControls(actions); + } + } + } else if (type == mtpc_messageActionStarGiftPurchaseOfferDeclined) { + const auto &data = action.c_messageActionStarGiftPurchaseOfferDeclined(); + UpdateComponents(HistoryServiceSuggestFinish::Bit()); + const auto finish = Get(); + finish->refundType = data.is_expired() + ? SuggestRefundType::Expired + : SuggestRefundType::User; + finish->price = CreditsAmountFromTL(data.vprice()); } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { @@ -6152,16 +6203,8 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { const auto isSelf = _from->isSelf(); const auto resale = CreditsAmountFromTL(action.vresale_amount()); const auto resaleCost = !resale - ? TextWithEntities() - : resale.stars() - ? TextWithEntities{ tr::lng_action_gift_for_stars( - tr::now, - lt_count, - resale.value()) } - : TextWithEntities{ tr::lng_action_gift_for_ton( - tr::now, - lt_count, - resale.value()) }; + ? tr::marked() + : tr::marked(PrepareCreditsAmountText(resale)); const auto giftPeer = action.vpeer() ? peerFromMTP(*action.vpeer()) : PeerId(); @@ -6414,11 +6457,91 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto prepareStarGiftPurchaseOffer = [&](const MTPDmessageActionStarGiftPurchaseOffer &action) { auto result = PreparedServiceText{}; + action.vgift().match([&](const MTPDstarGiftUnique &data) { + const auto amount = CreditsAmountFromTL(action.vprice()); + const auto cost = tr::marked(PrepareCreditsAmountText(amount)); + const auto giftName = tr::bold(qs(data.vtitle()) + + u" #"_q + + QString::number(data.vnum().v)); + if (_from->isSelf()) { + result.text = tr::lng_action_gift_offer_you( + tr::now, + lt_cost, + cost, + lt_name, + giftName, + tr::marked); + } else { + result.links.push_back(fromLink()); + result.text = tr::lng_action_gift_offer( + tr::now, + lt_user, + fromLinkText(), + lt_cost, + cost, + lt_name, + giftName, + tr::marked); + } + }, [](const MTPDstarGift &) { + }); return result; }; auto prepareStarGiftPurchaseOfferDeclined = [&](const MTPDmessageActionStarGiftPurchaseOfferDeclined &action) { auto result = PreparedServiceText{}; + action.vgift().match([&](const MTPDstarGiftUnique &data) { + const auto amount = CreditsAmountFromTL(action.vprice()); + const auto cost = tr::marked(PrepareCreditsAmountText(amount)); + const auto giftName = Ui::Text::Bold(qs(data.vtitle()) + + u" #"_q + + QString::number(data.vnum().v)); + const auto expired = action.is_expired(); + if (_from->isSelf()) { + result.links.push_back(_history->peer->createOpenLink()); + result.text = expired + ? tr::lng_action_gift_offer_expired( + tr::now, + lt_user, + tr::link(st::wrap_rtl(_history->peer->name()), 1), + lt_name, + giftName, + lt_cost, + cost, + tr::marked) + : tr::lng_action_gift_offer_declined_you( + tr::now, + lt_user, + tr::link(st::wrap_rtl(_history->peer->name()), 1), + lt_name, + giftName, + lt_cost, + cost, + tr::marked); + } else { + if (!expired) { + result.links.push_back(fromLink()); + } + result.text = expired + ? tr::lng_action_gift_offer_expired_your( + tr::now, + lt_name, + giftName, + lt_cost, + cost, + tr::marked) + : tr::lng_action_gift_offer_declined( + tr::now, + lt_user, + fromLinkText(), + lt_name, + giftName, + lt_cost, + cost, + tr::marked); + } + }, [](const MTPDstarGift &) { + }); return result; }; @@ -6749,6 +6872,18 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { fields.vyear().value_or_empty()).serialize(), .type = Data::GiftType::BirthdaySuggest, }); + }, [&](const MTPDmessageActionStarGiftPurchaseOffer &data) { + if (const auto suggestion = Get()) { + Assert(suggestion->gift != nullptr); + + _media = std::make_unique( + this, + _from, + Data::GiftCode{ + .unique = suggestion->gift, + .type = Data::GiftType::GiftOffer, + }); + } }, [](const auto &) { }); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 0cbe4001c4..5307289658 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -580,7 +580,8 @@ public: const HistoryMessageSuggestion *suggest) const; [[nodiscard]] SuggestionActions computeSuggestionActions( bool accepted, - bool rejected) const; + bool rejected, + TimeId giftOfferExpiresAt) const; [[nodiscard]] bool needsUpdateForVideoQualities(const MTPMessage &data); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 184a368706..048a69871e 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -1219,7 +1219,8 @@ bool HistoryMessageReplyMarkup::hiddenBy(Data::Media *media) const { void HistoryMessageReplyMarkup::updateSuggestControls( SuggestionActions actions) { - if (actions == SuggestionActions::AcceptAndDecline) { + if (actions == SuggestionActions::AcceptAndDecline + || actions == SuggestionActions::GiftOfferActions) { data.flags |= ReplyMarkupFlag::SuggestionAccept; } else { data.flags &= ~ReplyMarkupFlag::SuggestionAccept; @@ -1238,7 +1239,21 @@ void HistoryMessageReplyMarkup::updateSuggestControls( type, &HistoryMessageMarkupButton::type); }; - if (actions == SuggestionActions::AcceptAndDecline) { + if (actions == SuggestionActions::GiftOfferActions) { + if (has(Type::SuggestAccept)) { + // Nothing changed. + } + data.rows.push_back({ + { + Type::SuggestDecline, + tr::lng_action_gift_offer_decline(tr::now), + }, + { + Type::SuggestAccept, + tr::lng_action_gift_offer_accept(tr::now), + }, + }); + } else if (actions == SuggestionActions::AcceptAndDecline) { // ... rows ... // [decline] | [accept] // [suggestchanges] diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 07b974d1f9..031caeb598 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -62,6 +62,7 @@ enum class SuggestionActions : uchar { None, Decline, AcceptAndDecline, + GiftOfferActions, }; struct HistoryMessageVia : RuntimeComponent { @@ -624,12 +625,12 @@ struct HistoryMessageFactcheck struct HistoryMessageSuggestion : RuntimeComponent { + std::shared_ptr gift; CreditsAmount price; TimeId date = 0; mtpRequestId requestId = 0; bool accepted = false; bool rejected = false; - bool gift = false; }; struct HistoryMessageRestrictions @@ -714,12 +715,13 @@ enum class SuggestRefundType { None, User, Admin, + Expired, }; struct HistoryServiceSuggestFinish : RuntimeComponent , HistoryServiceDependentData { - CreditsAmount successPrice; + CreditsAmount price; SuggestRefundType refundType = SuggestRefundType::None; }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp index 215d39c322..e3cdbbe279 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp @@ -647,7 +647,7 @@ void ChooseSuggestPriceBox( durations, state->offerDuration.current()); SingleChoiceBox(box, { - .title = tr::lng_settings_angle_backend(), + .title = tr::lng_gift_offer_duration(), .options = options, .initialSelection = int(selected - begin(durations)), .callback = save, diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index f830c6e15e..8db91380f3 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/stickers_emoji_pack.h" #include "payments/payments_reaction_process.h" // TryAddingPaidReaction. #include "window/window_session_controller.h" +#include "ui/effects/glare.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/reaction_fly_animation.h" #include "ui/toast/toast.h" @@ -42,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/item_text_options.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/round_rect.h" #include "data/components/sponsored_messages.h" #include "data/data_channel.h" #include "data/data_saved_sublist.h" @@ -53,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_dialogs.h" namespace HistoryView { namespace { @@ -66,6 +69,287 @@ Element *HoveredLinkElement/* = nullptr*/; Element *PressedLinkElement/* = nullptr*/; Element *MousedElement/* = nullptr*/; +class KeyboardStyle : public ReplyKeyboard::Style { +public: + KeyboardStyle(const style::BotKeyboardButton &st, Fn repaint); + + Images::CornersMaskRef buttonRounding( + Ui::BubbleRounding outer, + RectParts sides) const override; + + void startPaint( + QPainter &p, + const Ui::ChatStyle *st) const override; + const style::TextStyle &textStyle() const override; + void repaint(not_null item) const override; + +protected: + void paintButtonBg( + QPainter &p, + const Ui::ChatStyle *st, + const QRect &rect, + Ui::BubbleRounding rounding, + float64 howMuchOver) const override; + void paintButtonIcon( + QPainter &p, + const Ui::ChatStyle *st, + const QRect &rect, + int outerWidth, + HistoryMessageMarkupButton::Type type) const override; + void paintButtonLoading( + QPainter &p, + const Ui::ChatStyle *st, + const QRect &rect, + int outerWidth, + Ui::BubbleRounding rounding) const override; + int minButtonWidth(HistoryMessageMarkupButton::Type type) const override; + +private: + struct CachedBg { + QImage image; + QColor color; + }; + using BubbleRoundingKey = uchar; + mutable base::flat_map _cachedBg; + mutable base::flat_map _cachedOutline; + mutable std::unique_ptr _glare; + Fn _repaint; + +}; + +KeyboardStyle::KeyboardStyle( + const style::BotKeyboardButton &st, + Fn repaint) +: ReplyKeyboard::Style(st) +, _repaint(std::move(repaint)) { +} + +void KeyboardStyle::startPaint( + QPainter &p, + const Ui::ChatStyle *st) const { + Expects(st != nullptr); + + p.setPen(st->msgServiceFg()); +} + +const style::TextStyle &KeyboardStyle::textStyle() const { + return st::serviceTextStyle; +} + +void KeyboardStyle::repaint(not_null item) const { + item->history()->owner().requestItemRepaint(item); +} + +Images::CornersMaskRef KeyboardStyle::buttonRounding( + Ui::BubbleRounding outer, + RectParts sides) const { + using namespace Images; + using namespace Ui; + using Radius = CachedCornerRadius; + using Corner = BubbleCornerRounding; + auto result = CornersMaskRef(CachedCornersMasks(Radius::BubbleSmall)); + if (sides & RectPart::Bottom) { + const auto &large = CachedCornersMasks(Radius::BubbleLarge); + auto round = [&](RectPart side, int index) { + if ((sides & side) && (outer[index] == Corner::Large)) { + result.p[index] = &large[index]; + } + }; + round(RectPart::Left, kBottomLeft); + round(RectPart::Right, kBottomRight); + } + return result; +} + +void KeyboardStyle::paintButtonBg( + QPainter &p, + const Ui::ChatStyle *st, + const QRect &rect, + Ui::BubbleRounding rounding, + float64 howMuchOver) const { + Expects(st != nullptr); + + using Corner = Ui::BubbleCornerRounding; + auto &cachedBg = _cachedBg[rounding.key()]; + + const auto sti = &st->imageStyle(false); + const auto ratio = style::DevicePixelRatio(); + if (cachedBg.image.isNull() + || cachedBg.image.width() != (rect.width() * ratio) + || cachedBg.color != sti->msgServiceBg->c) { + cachedBg.color = sti->msgServiceBg->c; + cachedBg.image = QImage( + rect.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + cachedBg.image.setDevicePixelRatio(ratio); + cachedBg.image.fill(Qt::transparent); + { + auto painter = QPainter(&cachedBg.image); + + const auto &small = sti->msgServiceBgCornersSmall; + const auto &large = sti->msgServiceBgCornersLarge; + auto corners = Ui::CornersPixmaps(); + int radiuses[4]; + for (auto i = 0; i != 4; ++i) { + const auto isLarge = (rounding[i] == Corner::Large); + corners.p[i] = (isLarge ? large : small).p[i]; + radiuses[i] = Ui::CachedCornerRadiusValue(isLarge + ? Ui::CachedCornerRadius::BubbleLarge + : Ui::CachedCornerRadius::BubbleSmall); + } + const auto r = Rect(rect.size()); + _cachedOutline[rounding.key()] = Ui::ComplexRoundedRectPath( + r - Margins(st::lineWidth), + radiuses[0], + radiuses[1], + radiuses[2], + radiuses[3]); + Ui::FillRoundRect(painter, r, sti->msgServiceBg, corners); + } + } + p.drawImage(rect.topLeft(), cachedBg.image); + if (howMuchOver > 0) { + auto o = p.opacity(); + p.setOpacity(o * howMuchOver); + const auto &small = st->msgBotKbOverBgAddCornersSmall(); + const auto &large = st->msgBotKbOverBgAddCornersLarge(); + auto over = Ui::CornersPixmaps(); + for (auto i = 0; i != 4; ++i) { + over.p[i] = (rounding[i] == Corner::Large ? large : small).p[i]; + } + Ui::FillRoundRect(p, rect, st->msgBotKbOverBgAdd(), over); + p.setOpacity(o); + } +} + +void KeyboardStyle::paintButtonIcon( + QPainter &p, + const Ui::ChatStyle *st, + const QRect &rect, + int outerWidth, + HistoryMessageMarkupButton::Type type) const { + Expects(st != nullptr); + + using Type = HistoryMessageMarkupButton::Type; + const auto icon = [&]() -> const style::icon* { + switch (type) { + case Type::Url: + case Type::Auth: return &st->msgBotKbUrlIcon(); + case Type::Buy: return &st->msgBotKbPaymentIcon(); + case Type::SwitchInlineSame: + case Type::SwitchInline: return &st->msgBotKbSwitchPmIcon(); + case Type::WebView: + case Type::SimpleWebView: return &st->msgBotKbWebviewIcon(); + case Type::CopyText: return &st->msgBotKbCopyIcon(); + } + return nullptr; + }(); + if (icon) { + icon->paint(p, rect.x() + rect.width() - icon->width() - st::msgBotKbIconPadding, rect.y() + st::msgBotKbIconPadding, outerWidth); + } +} + +void KeyboardStyle::paintButtonLoading( + QPainter &p, + const Ui::ChatStyle *st, + const QRect &rect, + int outerWidth, + Ui::BubbleRounding rounding) const { + Expects(st != nullptr); + + if (anim::Disabled()) { + const auto &icon = st->historySendingInvertedIcon(); + icon.paint( + p, + rect::right(rect) - icon.width() - st::msgBotKbIconPadding, + rect::bottom(rect) - icon.height() - st::msgBotKbIconPadding, + rect.x() * 2 + rect.width()); + return; + } + + const auto cacheKey = rounding.key(); + auto &cachedBg = _cachedBg[cacheKey]; + if (!cachedBg.image.isNull()) { + if (_glare && _glare->glare.birthTime) { + const auto progress = _glare->progress(crl::now()); + const auto w = _glare->width; + const auto h = rect.height(); + const auto x = (-w) + (w * 2) * progress; + + auto frame = cachedBg.image; + frame.fill(Qt::transparent); + { + auto painter = QPainter(&frame); + auto hq = PainterHighQualityEnabler(painter); + painter.setPen(Qt::NoPen); + painter.drawTiledPixmap(x, 0, w, h, _glare->pixmap, 0, 0); + + auto path = QPainterPath(); + path.addRect(Rect(rect.size())); + path -= _cachedOutline[cacheKey]; + + constexpr auto kBgOutlineAlpha = 0.5; + constexpr auto kFgOutlineAlpha = 0.8; + const auto &c = st::premiumButtonFg->c; + painter.setPen(Qt::NoPen); + painter.setBrush(c); + painter.setOpacity(kBgOutlineAlpha); + painter.drawPath(path); + auto gradient = QLinearGradient(-w, 0, w * 2, 0); + { + constexpr auto kShiftLeft = 0.01; + constexpr auto kShiftRight = 0.99; + auto stops = _glare->computeGradient(c).stops(); + stops[1] = { + std::clamp(progress, kShiftLeft, kShiftRight), + QColor(c.red(), c.green(), c.blue(), kFgOutlineAlpha), + }; + gradient.setStops(std::move(stops)); + } + painter.setBrush(QBrush(gradient)); + painter.setOpacity(1); + painter.drawPath(path); + + painter.setCompositionMode( + QPainter::CompositionMode_DestinationIn); + painter.drawImage(0, 0, cachedBg.image); + } + p.drawImage(rect.x(), rect.y(), frame); + } else { + _glare = std::make_unique(); + _glare->width = outerWidth; + + constexpr auto kTimeout = crl::time(0); + constexpr auto kDuration = crl::time(1100); + const auto color = st::premiumButtonFg->c; + _glare->validate(color, _repaint, kTimeout, kDuration); + } + } +} + +int KeyboardStyle::minButtonWidth( + HistoryMessageMarkupButton::Type type) const { + using Type = HistoryMessageMarkupButton::Type; + int result = 2 * buttonPadding(), iconWidth = 0; + switch (type) { + case Type::Url: + case Type::Auth: iconWidth = st::msgBotKbUrlIcon.width(); break; + case Type::Buy: iconWidth = st::msgBotKbPaymentIcon.width(); break; + case Type::SwitchInlineSame: + case Type::SwitchInline: iconWidth = st::msgBotKbSwitchPmIcon.width(); break; + case Type::Callback: + case Type::CallbackWithPassword: + case Type::Game: iconWidth = st::historySendingInvertedIcon.width(); break; + case Type::WebView: + case Type::SimpleWebView: iconWidth = st::msgBotKbWebviewIcon.width(); break; + case Type::CopyText: return st::msgBotKbCopyIcon.width(); break; + } + if (iconWidth > 0) { + result = std::max(result, 2 * iconWidth + 4 * int(st::msgBotKbIconPadding)); + } + return result; +} + [[nodiscard]] bool IsAttachedToPreviousInSavedMessages( not_null previous, HistoryMessageForwarded *prevForwarded, @@ -1463,6 +1747,24 @@ void Element::validateTextSkipBlock(bool has, int width, int height) { } } +void Element::validateInlineKeyboard(HistoryMessageReplyMarkup *markup) { + if (!markup + || markup->inlineKeyboard + || markup->hiddenBy(data()->media())) { + return; + } + const auto item = data(); + //if (item->hideLinks()) { + // item->setHasHiddenLinks(true); + // return; + //} + markup->inlineKeyboard = std::make_unique( + item, + std::make_unique( + st::msgBotKbButton, + [=] { item->history()->owner().requestItemRepaint(item); })); +} + void Element::previousInBlocksChanged() { recountThreadBarInBlocks(); recountDisplayDateInBlocks(); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 74541332c0..e7f2a67c17 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -18,6 +18,7 @@ class HistoryBlock; class HistoryItem; struct HistoryMessageReply; struct PreparedServiceText; +struct HistoryMessageReplyMarkup; namespace Data { class Thread; @@ -693,6 +694,7 @@ protected: [[nodiscard]] int textHeightFor(int textWidth); void validateText(); void validateTextSkipBlock(bool has, int width, int height); + void validateInlineKeyboard(HistoryMessageReplyMarkup *markup); void clearSpecialOnlyEmoji(); void checkSpecialOnlyEmoji(); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index c6ce7c0930..a5c781b437 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -26,13 +26,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "boxes/premium_preview_box.h" #include "boxes/share_box.h" -#include "ui/effects/glare.h" #include "ui/effects/reaction_fly_animation.h" #include "ui/text/text_utilities.h" #include "ui/text/text_extended_data.h" #include "ui/power_saving.h" #include "ui/rect.h" -#include "ui/round_rect.h" +//#include "ui/round_rect.h" #include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" #include "data/data_session.h" @@ -59,286 +58,6 @@ namespace { constexpr auto kPlayStatusLimit = 2; const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_"; -class KeyboardStyle : public ReplyKeyboard::Style { -public: - KeyboardStyle(const style::BotKeyboardButton &st, Fn repaint); - - Images::CornersMaskRef buttonRounding( - Ui::BubbleRounding outer, - RectParts sides) const override; - - void startPaint( - QPainter &p, - const Ui::ChatStyle *st) const override; - const style::TextStyle &textStyle() const override; - void repaint(not_null item) const override; - -protected: - void paintButtonBg( - QPainter &p, - const Ui::ChatStyle *st, - const QRect &rect, - Ui::BubbleRounding rounding, - float64 howMuchOver) const override; - void paintButtonIcon( - QPainter &p, - const Ui::ChatStyle *st, - const QRect &rect, - int outerWidth, - HistoryMessageMarkupButton::Type type) const override; - void paintButtonLoading( - QPainter &p, - const Ui::ChatStyle *st, - const QRect &rect, - int outerWidth, - Ui::BubbleRounding rounding) const override; - int minButtonWidth(HistoryMessageMarkupButton::Type type) const override; - -private: - using BubbleRoundingKey = uchar; - mutable base::flat_map _cachedBg; - mutable base::flat_map _cachedOutline; - mutable std::unique_ptr _glare; - Fn _repaint; - rpl::lifetime _lifetime; - -}; - -KeyboardStyle::KeyboardStyle( - const style::BotKeyboardButton &st, - Fn repaint) -: ReplyKeyboard::Style(st) -, _repaint(std::move(repaint)) { - style::PaletteChanged( - ) | rpl::start_with_next([=] { - _cachedBg = {}; - _cachedOutline = {}; - }, _lifetime); -} - -void KeyboardStyle::startPaint( - QPainter &p, - const Ui::ChatStyle *st) const { - Expects(st != nullptr); - - p.setPen(st->msgServiceFg()); -} - -const style::TextStyle &KeyboardStyle::textStyle() const { - return st::serviceTextStyle; -} - -void KeyboardStyle::repaint(not_null item) const { - item->history()->owner().requestItemRepaint(item); -} - -Images::CornersMaskRef KeyboardStyle::buttonRounding( - Ui::BubbleRounding outer, - RectParts sides) const { - using namespace Images; - using namespace Ui; - using Radius = CachedCornerRadius; - using Corner = BubbleCornerRounding; - auto result = CornersMaskRef(CachedCornersMasks(Radius::BubbleSmall)); - if (sides & RectPart::Bottom) { - const auto &large = CachedCornersMasks(Radius::BubbleLarge); - auto round = [&](RectPart side, int index) { - if ((sides & side) && (outer[index] == Corner::Large)) { - result.p[index] = &large[index]; - } - }; - round(RectPart::Left, kBottomLeft); - round(RectPart::Right, kBottomRight); - } - return result; -} - -void KeyboardStyle::paintButtonBg( - QPainter &p, - const Ui::ChatStyle *st, - const QRect &rect, - Ui::BubbleRounding rounding, - float64 howMuchOver) const { - Expects(st != nullptr); - - using Corner = Ui::BubbleCornerRounding; - auto &cachedBg = _cachedBg[rounding.key()]; - - if (cachedBg.isNull() - || cachedBg.width() != (rect.width() * style::DevicePixelRatio())) { - cachedBg = QImage( - rect.size() * style::DevicePixelRatio(), - QImage::Format_ARGB32_Premultiplied); - cachedBg.setDevicePixelRatio(style::DevicePixelRatio()); - cachedBg.fill(Qt::transparent); - { - auto painter = QPainter(&cachedBg); - - const auto sti = &st->imageStyle(false); - const auto &small = sti->msgServiceBgCornersSmall; - const auto &large = sti->msgServiceBgCornersLarge; - auto corners = Ui::CornersPixmaps(); - int radiuses[4]; - for (auto i = 0; i != 4; ++i) { - const auto isLarge = (rounding[i] == Corner::Large); - corners.p[i] = (isLarge ? large : small).p[i]; - radiuses[i] = Ui::CachedCornerRadiusValue(isLarge - ? Ui::CachedCornerRadius::BubbleLarge - : Ui::CachedCornerRadius::BubbleSmall); - } - const auto r = Rect(rect.size()); - _cachedOutline[rounding.key()] = Ui::ComplexRoundedRectPath( - r - Margins(st::lineWidth), - radiuses[0], - radiuses[1], - radiuses[2], - radiuses[3]); - Ui::FillRoundRect(painter, r, sti->msgServiceBg, corners); - } - } - p.drawImage(rect.topLeft(), cachedBg); - if (howMuchOver > 0) { - auto o = p.opacity(); - p.setOpacity(o * howMuchOver); - const auto &small = st->msgBotKbOverBgAddCornersSmall(); - const auto &large = st->msgBotKbOverBgAddCornersLarge(); - auto over = Ui::CornersPixmaps(); - for (auto i = 0; i != 4; ++i) { - over.p[i] = (rounding[i] == Corner::Large ? large : small).p[i]; - } - Ui::FillRoundRect(p, rect, st->msgBotKbOverBgAdd(), over); - p.setOpacity(o); - } -} - -void KeyboardStyle::paintButtonIcon( - QPainter &p, - const Ui::ChatStyle *st, - const QRect &rect, - int outerWidth, - HistoryMessageMarkupButton::Type type) const { - Expects(st != nullptr); - - using Type = HistoryMessageMarkupButton::Type; - const auto icon = [&]() -> const style::icon* { - switch (type) { - case Type::Url: - case Type::Auth: return &st->msgBotKbUrlIcon(); - case Type::Buy: return &st->msgBotKbPaymentIcon(); - case Type::SwitchInlineSame: - case Type::SwitchInline: return &st->msgBotKbSwitchPmIcon(); - case Type::WebView: - case Type::SimpleWebView: return &st->msgBotKbWebviewIcon(); - case Type::CopyText: return &st->msgBotKbCopyIcon(); - } - return nullptr; - }(); - if (icon) { - icon->paint(p, rect.x() + rect.width() - icon->width() - st::msgBotKbIconPadding, rect.y() + st::msgBotKbIconPadding, outerWidth); - } -} - -void KeyboardStyle::paintButtonLoading( - QPainter &p, - const Ui::ChatStyle *st, - const QRect &rect, - int outerWidth, - Ui::BubbleRounding rounding) const { - Expects(st != nullptr); - - if (anim::Disabled()) { - const auto &icon = st->historySendingInvertedIcon(); - icon.paint( - p, - rect::right(rect) - icon.width() - st::msgBotKbIconPadding, - rect::bottom(rect) - icon.height() - st::msgBotKbIconPadding, - rect.x() * 2 + rect.width()); - return; - } - - const auto cacheKey = rounding.key(); - auto &cachedBg = _cachedBg[cacheKey]; - if (!cachedBg.isNull()) { - if (_glare && _glare->glare.birthTime) { - const auto progress = _glare->progress(crl::now()); - const auto w = _glare->width; - const auto h = rect.height(); - const auto x = (-w) + (w * 2) * progress; - - auto frame = cachedBg; - frame.fill(Qt::transparent); - { - auto painter = QPainter(&frame); - auto hq = PainterHighQualityEnabler(painter); - painter.setPen(Qt::NoPen); - painter.drawTiledPixmap(x, 0, w, h, _glare->pixmap, 0, 0); - - auto path = QPainterPath(); - path.addRect(Rect(rect.size())); - path -= _cachedOutline[cacheKey]; - - constexpr auto kBgOutlineAlpha = 0.5; - constexpr auto kFgOutlineAlpha = 0.8; - const auto &c = st::premiumButtonFg->c; - painter.setPen(Qt::NoPen); - painter.setBrush(c); - painter.setOpacity(kBgOutlineAlpha); - painter.drawPath(path); - auto gradient = QLinearGradient(-w, 0, w * 2, 0); - { - constexpr auto kShiftLeft = 0.01; - constexpr auto kShiftRight = 0.99; - auto stops = _glare->computeGradient(c).stops(); - stops[1] = { - std::clamp(progress, kShiftLeft, kShiftRight), - QColor(c.red(), c.green(), c.blue(), kFgOutlineAlpha), - }; - gradient.setStops(std::move(stops)); - } - painter.setBrush(QBrush(gradient)); - painter.setOpacity(1); - painter.drawPath(path); - - painter.setCompositionMode( - QPainter::CompositionMode_DestinationIn); - painter.drawImage(0, 0, cachedBg); - } - p.drawImage(rect.x(), rect.y(), frame); - } else { - _glare = std::make_unique(); - _glare->width = outerWidth; - - constexpr auto kTimeout = crl::time(0); - constexpr auto kDuration = crl::time(1100); - const auto color = st::premiumButtonFg->c; - _glare->validate(color, _repaint, kTimeout, kDuration); - } - } -} - -int KeyboardStyle::minButtonWidth( - HistoryMessageMarkupButton::Type type) const { - using Type = HistoryMessageMarkupButton::Type; - int result = 2 * buttonPadding(), iconWidth = 0; - switch (type) { - case Type::Url: - case Type::Auth: iconWidth = st::msgBotKbUrlIcon.width(); break; - case Type::Buy: iconWidth = st::msgBotKbPaymentIcon.width(); break; - case Type::SwitchInlineSame: - case Type::SwitchInline: iconWidth = st::msgBotKbSwitchPmIcon.width(); break; - case Type::Callback: - case Type::CallbackWithPassword: - case Type::Game: iconWidth = st::historySendingInvertedIcon.width(); break; - case Type::WebView: - case Type::SimpleWebView: iconWidth = st::msgBotKbWebviewIcon.width(); break; - case Type::CopyText: return st::msgBotKbCopyIcon.width(); break; - } - if (iconWidth > 0) { - result = std::max(result, 2 * iconWidth + 4 * int(st::msgBotKbIconPadding)); - } - return result; -} - QString FastForwardText() { return u"Forward"_q; } @@ -3507,24 +3226,6 @@ bool Message::embedReactionsInBubble() const { return needInfoDisplay(); } -void Message::validateInlineKeyboard(HistoryMessageReplyMarkup *markup) { - if (!markup - || markup->inlineKeyboard - || markup->hiddenBy(data()->media())) { - return; - } - const auto item = data(); - //if (item->hideLinks()) { - // item->setHasHiddenLinks(true); - // return; - //} - markup->inlineKeyboard = std::make_unique( - item, - std::make_unique( - st::msgBotKbButton, - [=] { item->history()->owner().requestItemRepaint(item); })); -} - void Message::validateFromNameText(PeerData *from) const { if (!from) { if (_fromNameStatus) { diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index e07d40435f..08d6be246f 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -292,7 +292,6 @@ private: void refreshInfoSkipBlock(HistoryItem *textItem); [[nodiscard]] int monospaceMaxWidth() const; - void validateInlineKeyboard(HistoryMessageReplyMarkup *markup); void updateViewButtonExistence(); [[nodiscard]] int viewButtonHeight() const; diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index d2ceacc08f..a0a051949f 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -200,6 +200,15 @@ void SetText(Ui::Text::String &text, const QString &content) { text.setText(st::serviceTextStyle, content, EmptyLineOptions); } +[[nodiscard]] Ui::BubbleRounding KeyboardRounding() { + return Ui::BubbleRounding{ + .topLeft = Ui::BubbleCornerRounding::Large, + .topRight = Ui::BubbleCornerRounding::Large, + .bottomLeft = Ui::BubbleCornerRounding::Large, + .bottomRight = Ui::BubbleCornerRounding::Large, + }; +} + } // namespace int WideChatWidth() { @@ -411,6 +420,20 @@ Service::Service( setupReactions(replacing); } +void Service::clickHandlerPressedChanged( + const ClickHandlerPtr &handler, + bool pressed) { + if (const auto markup = data()->Get()) { + if (const auto keyboard = markup->inlineKeyboard.get()) { + keyboard->clickHandlerPressedChanged( + handler, + pressed, + KeyboardRounding()); + } + } + Element::clickHandlerPressedChanged(handler, pressed); +} + QRect Service::innerGeometry() const { return countGeometry(); } @@ -439,6 +462,14 @@ void Service::animateReaction(Ui::ReactionFlyAnimationArgs &&args) { } const auto repainter = [=] { repaint(); }; + const auto item = data(); + const auto keyboard = item->inlineReplyKeyboard(); + auto keyboardHeight = 0; + if (keyboard) { + keyboardHeight = keyboard->naturalHeight(); + g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); + } + if (_reactions) { const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height(); const auto reactionsLeft = 0; @@ -496,11 +527,21 @@ QSize Service::performCountCurrentSize(int newWidth) { } } + const auto item = data(); + if (const auto keyboard = item->inlineReplyKeyboard()) { + const auto keyboardWidth = mediaDisplayed ? media->width() : contentWidth; + const auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight(); + newHeight += keyboardHeight; + keyboard->resize(keyboardWidth, keyboardHeight - st::msgBotKbButton.margin); + } + return { newWidth, newHeight }; } QSize Service::performCountOptimalSize() { + const auto markup = data()->inlineReplyMarkup(); validateText(); + validateInlineKeyboard(markup); if (_reactions) { _reactions->initDimensions(); @@ -591,6 +632,31 @@ void Service::draw(Painter &p, const PaintContext &context) const { const auto mediaDisplayed = media && media->isDisplayed(); const auto onlyMedia = (mediaDisplayed && media->hideServiceText()); + const auto item = data(); + const auto keyboard = item->inlineReplyKeyboard(); + const auto fullGeometry = g; + if (keyboard) { + // We need to count geometry without keyboard for bubble selection + // intervals counting below. + const auto keyboardHeight = st::msgBotKbButton.margin + keyboard->naturalHeight(); + g.setHeight(g.height() - keyboardHeight); + } + + if (keyboard) { + const auto keyboardWidth = mediaDisplayed ? media->width() : g.width(); + const auto keyboardPosition = QPoint( + g.left() + (g.width() - keyboardWidth) / 2, + g.top() + g.height() + st::msgBotKbButton.margin); + p.translate(keyboardPosition); + keyboard->paint( + p, + context.st, + KeyboardRounding(), + keyboardWidth, + context.clip.translated(-keyboardPosition)); + p.translate(-keyboardPosition); + } + if (_reactions) { const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height(); const auto reactionsLeft = 0; @@ -680,6 +746,26 @@ TextState Service::textState(QPoint point, StateRequest request) const { return result; } + auto keyboard = item->inlineReplyKeyboard(); + auto keyboardHeight = 0; + if (keyboard) { + keyboardHeight = keyboard->naturalHeight(); + g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight); + + if (item->isHistoryEntry()) { + const auto keyboardWidth = mediaDisplayed ? media->width() : g.width(); + const auto keyboardPosition = QPoint( + g.left() + (g.width() - keyboardWidth) / 2, + g.top() + g.height() + st::msgBotKbButton.margin); + if (QRect(keyboardPosition, QSize(keyboardWidth, keyboardHeight)).contains(point)) { + result.link = keyboard->getLink(point - keyboardPosition); + if (result.link) { + return result; + } + } + } + } + if (const auto service = Get()) { result.link = service->textState(point, request, g); if (result.link) { @@ -698,7 +784,6 @@ TextState Service::textState(QPoint point, StateRequest request) const { } } - if (onlyMedia) { return media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->width()) / 2, g.top()), request); } else if (mediaDisplayed) { diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.h b/Telegram/SourceFiles/history/view/history_view_service_message.h index 57dabbeccb..a4adae0b51 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.h +++ b/Telegram/SourceFiles/history/view/history_view_service_message.h @@ -33,6 +33,10 @@ public: not_null data, Element *replacing); + void clickHandlerPressedChanged( + const ClickHandlerPtr &handler, + bool pressed) override; + int marginTop() const override; int marginBottom() const override; bool isHidden() const override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp index 5b5864b2a6..7b4b309e93 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp @@ -8,11 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_service_box.h" #include "core/ui_integration.h" +#include "data/data_session.h" #include "history/view/media/history_view_sticker_player_abstract.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_element.h" #include "history/view/history_view_text_helper.h" #include "history/history.h" +#include "history/history_item.h" #include "lang/lang_keys.h" #include "ui/chat/chat_style.h" #include "ui/effects/animation_value.h" @@ -122,10 +124,51 @@ ServiceBox::ServiceBox( *type); _button.lastFg = std::make_unique(); } + + if (auto changes = _content->changes()) { + std::move(changes) | rpl::start_with_next([=] { + applyContentChanges(); + }, _lifetime); + } } ServiceBox::~ServiceBox() = default; +void ServiceBox::applyContentChanges() { + const auto subtitleWas = _subtitle.countHeight(_maxWidth); + + const auto parent = _parent; + _subtitle = Ui::Text::String( + st::premiumPreviewAbout.style, + Ui::Text::Filtered( + _content->subtitle(), + { + EntityType::Bold, + EntityType::StrikeOut, + EntityType::Underline, + EntityType::Italic, + EntityType::Spoiler, + EntityType::CustomEmoji, + }), + kMarkupTextOptions, + _maxWidth, + Core::TextContext({ + .session = &parent->history()->session(), + .repaint = [parent] { parent->customEmojiRepaint(); }, + })); + InitElementTextPart(parent, _subtitle); + const auto subtitleNow = _subtitle.countHeight(_maxWidth); + if (subtitleNow != subtitleWas) { + _size.setHeight(_size.height() - subtitleWas + subtitleNow); + _innerSize = _size - QSize(0, st::msgServiceGiftBoxTopSkip); + + const auto item = parent->data(); + item->history()->owner().requestItemResize(item); + } else { + parent->repaint(); + } +} + QSize ServiceBox::countOptimalSize() { return _size; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.h b/Telegram/SourceFiles/history/view/media/history_view_service_box.h index b15ded3306..235ed2a4ca 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.h +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.h @@ -36,6 +36,12 @@ public: return top(); } [[nodiscard]] virtual rpl::producer button() = 0; + + // For now only subtitle() changes are observed. + [[nodiscard]] virtual rpl::producer<> changes() { + return nullptr; + } + [[nodiscard]] virtual auto buttonMinistars() -> std::optional { return std::nullopt; @@ -105,6 +111,8 @@ private: [[nodiscard]] QRect buttonRect() const; [[nodiscard]] QRect contentRect() const; + void applyContentChanges(); + const not_null _parent; const std::unique_ptr _content; mutable ClickHandlerPtr _contentLink; @@ -131,8 +139,8 @@ private: Ui::Text::String _title; Ui::Text::String _author; Ui::Text::String _subtitle; - const QSize _size; - const QSize _innerSize; + QSize _size; + QSize _innerSize; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp index ee912cf9a3..bb1f81b017 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp @@ -8,9 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_theme_document.h" #include "apiwrap.h" +#include "base/unixtime.h" #include "boxes/background_preview_box.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_item_components.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" #include "history/view/media/history_view_sticker_player_abstract.h" @@ -609,69 +611,181 @@ void ThemeDocumentBox::unloadHeavyPart() { } } -GiftThemeBox::GiftThemeBox( +GiftServiceBox::GiftServiceBox( not_null parent, not_null gift) : _parent(parent) , _data(*gift->gift()) { } -GiftThemeBox::~GiftThemeBox() = default; +GiftServiceBox::~GiftServiceBox() = default; -int GiftThemeBox::top() { +int GiftServiceBox::top() { return st::msgServiceStarGiftStickerTop; } -int GiftThemeBox::width() { +int GiftServiceBox::width() { return st::msgServiceStarGiftBoxWidth; } -QSize GiftThemeBox::size() { +QSize GiftServiceBox::size() { return QSize( st::msgServiceGiftThemeStickerSize, st::msgServiceGiftThemeStickerSize).grownBy( st::msgServiceGiftThemeStickerPadding); } -TextWithEntities GiftThemeBox::title() { +TextWithEntities GiftServiceBox::title() { return {}; } -TextWithEntities GiftThemeBox::subtitle() { - const auto giftName = Ui::Text::Bold( - Data::UniqueGiftName(*_data.unique)); - if (_parent->data()->out()) { +TextWithEntities GiftServiceBox::subtitle() { + const auto giftName = tr::bold(Data::UniqueGiftName(*_data.unique)); + if (_data.type == Data::GiftType::GiftOffer) { + const auto item = _parent->data(); + if (const auto suggestion = item->Get()) { + const auto amount = suggestion->price; + const auto cost = tr::bold(PrepareCreditsAmountText(amount)); + auto text = tr::marked(); + if (_parent->data()->out()) { + text.append(tr::lng_action_gift_offer_you( + tr::now, + lt_cost, + cost, + lt_name, + giftName, + tr::marked)); + } else { + text.append(tr::lng_action_gift_offer( + tr::now, + lt_user, + tr::bold(_parent->data()->from()->shortName()), + lt_cost, + cost, + lt_name, + giftName, + tr::marked)); + } + text.append(u"\n\n"_q); + + const auto ends = suggestion->date; + const auto now = base::unixtime::now(); + const auto expired = (now >= ends); + + checkKeyboardRemoval(suggestion, expired); + + if (suggestion->accepted) { + text.append( + tr::lng_action_gift_offer_state_accepted(tr::now)); + } else if (suggestion->rejected) { + text.append( + tr::lng_action_gift_offer_state_rejected(tr::now)); + } else { + if (expired) { + text.append( + tr::lng_action_gift_offer_state_expired(tr::now)); + } else { + auto time = QString(); + const auto left = (ends - now) + 59; + if (left >= 3600) { + const auto hours = left / 3600; + const auto minutes = (left % 3600) / 60; + time = tr::lng_action_gift_offer_time_medium( + tr::now, + lt_hours, + QString::number(hours), + lt_minutes, + QString::number(minutes)); + } else { + const auto minutes = left / 60; + time = tr::lng_action_gift_offer_time_small( + tr::now, + lt_minutes, + QString::number(minutes)); + } + text.append(tr::lng_action_gift_offer_state_expires( + tr::now, + lt_time, + tr::bold(time), + tr::marked)); + + const auto tillNext = left % 60; + _changeTimer.setCallback([=] { _changes.fire({}); }); + _changeTimer.callOnce((tillNext ? tillNext : 60) + * crl::time(1000)); + } + } + return text; + } + } else if (_parent->data()->out()) { return tr::lng_action_you_gift_theme_changed( tr::now, lt_name, giftName, - Ui::Text::WithEntities); + tr::marked); } else { return tr::lng_action_gift_theme_changed( tr::now, lt_from, - Ui::Text::Bold(_parent->data()->from()->shortName()), + tr::bold(_parent->data()->from()->shortName()), lt_name, giftName, - Ui::Text::WithEntities); + tr::marked); } return _parent->data()->originalText(); } -rpl::producer GiftThemeBox::button() { +void GiftServiceBox::checkKeyboardRemoval( + not_null suggestion, + bool expired) { + Expects(_data.type == Data::GiftType::GiftOffer); + + const auto item = _parent->data(); + if (const auto markup = item->Get()) { + if (!markup->data.isTrivial()) { + if (suggestion->accepted || suggestion->rejected || expired) { + crl::on_main(this, [=] { + clearKeyboard(); + }); + } + } + } +} + +void GiftServiceBox::clearKeyboard() { + Expects(_data.type == Data::GiftType::GiftOffer); + + const auto item = _parent->data(); + if (const auto markup = item->Get()) { + markup->updateSuggestControls(SuggestionActions::None); + item->history()->owner().requestItemResize(item); + } +} + +rpl::producer<> GiftServiceBox::changes() { + if (_data.type != Data::GiftType::GiftOffer) { + return nullptr; + } + return _changes.events(); +} + +rpl::producer GiftServiceBox::button() { + if (_data.type == Data::GiftType::GiftOffer) { + return nullptr; + } return tr::lng_sticker_premium_view(); } -ClickHandlerPtr GiftThemeBox::createViewLink() { +ClickHandlerPtr GiftServiceBox::createViewLink() { return std::make_shared( u"tg://nft?slug="_q + _data.unique->slug); } -int GiftThemeBox::buttonSkip() { +int GiftServiceBox::buttonSkip() { return st::msgServiceGiftBoxButtonMargins.top(); } -void GiftThemeBox::cacheUniqueBackground(int width, int height) { +void GiftServiceBox::cacheUniqueBackground(int width, int height) { if (!_patternEmoji) { const auto session = &_parent->data()->history()->session(); _patternEmoji = session->data().customEmojiManager().create( @@ -717,7 +831,7 @@ void GiftThemeBox::cacheUniqueBackground(int width, int height) { } } -void GiftThemeBox::draw( +void GiftServiceBox::draw( Painter &p, const PaintContext &context, const QRect &geometry) { @@ -734,17 +848,17 @@ void GiftThemeBox::draw( } } -bool GiftThemeBox::hideServiceText() { +bool GiftServiceBox::hideServiceText() { return true; } -void GiftThemeBox::stickerClearLoopPlayed() { +void GiftServiceBox::stickerClearLoopPlayed() { if (_sticker) { _sticker->stickerClearLoopPlayed(); } } -std::unique_ptr GiftThemeBox::stickerTakePlayer( +std::unique_ptr GiftServiceBox::stickerTakePlayer( not_null data, const Lottie::ColorReplacements *replacements) { return _sticker @@ -752,17 +866,17 @@ std::unique_ptr GiftThemeBox::stickerTakePlayer( : nullptr; } -bool GiftThemeBox::hasHeavyPart() { +bool GiftServiceBox::hasHeavyPart() { return (_sticker ? _sticker->hasHeavyPart() : false); } -void GiftThemeBox::unloadHeavyPart() { +void GiftServiceBox::unloadHeavyPart() { if (_sticker) { _sticker->unloadHeavyPart(); } } -void GiftThemeBox::ensureStickerCreated() const { +void GiftServiceBox::ensureStickerCreated() const { if (_sticker) { return; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h index d2f4079c30..23006073fb 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_sticker.h" class Image; +struct HistoryMessageSuggestion; namespace Data { class DocumentMedia; @@ -131,12 +132,14 @@ private: }; -class GiftThemeBox final : public ServiceBoxContent { +class GiftServiceBox final + : public ServiceBoxContent + , public base::has_weak_ptr { public: - GiftThemeBox( + GiftServiceBox( not_null parent, not_null gift); - ~GiftThemeBox(); + ~GiftServiceBox(); int top() override; int width() override; @@ -151,6 +154,8 @@ public: const QRect &geometry) override; ClickHandlerPtr createViewLink() override; + rpl::producer<> changes() override; + bool hideServiceText() override; void stickerClearLoopPlayed() override; std::unique_ptr stickerTakePlayer( @@ -163,6 +168,10 @@ public: private: void ensureStickerCreated() const; void cacheUniqueBackground(int width, int height); + void checkKeyboardRemoval( + not_null suggestion, + bool expired); + void clearKeyboard(); const not_null _parent; const Data::GiftCode &_data; @@ -171,6 +180,8 @@ private: base::flat_map _patternCache; bool _backroundPatterned = false; mutable std::optional _sticker; + rpl::event_stream<> _changes; + base::Timer _changeTimer; };