Show Reject/Accept buttons for offers.

This commit is contained in:
John Preston
2025-11-28 17:28:27 +04:00
parent 6fccbf036c
commit 1e89ee4e50
21 changed files with 796 additions and 354 deletions
+3 -1
View File
@@ -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.";
@@ -546,6 +546,7 @@ std::shared_ptr<ClickHandler> DeclineClickHandler(
if (!item) {
return;
}
RequestDeclineComment(controller->uiShow(), item);
});
}
@@ -181,3 +181,5 @@ private:
[[nodiscard]] CreditsAmount CreditsAmountFromTL(
const MTPStarsAmount *amount);
[[nodiscard]] MTPStarsAmount StarsAmountToTL(CreditsAmount amount);
[[nodiscard]] QString PrepareCreditsAmountText(CreditsAmount amount);
@@ -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());
}
@@ -2606,10 +2606,11 @@ std::unique_ptr<HistoryView::Media> 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<HistoryView::ServiceBox>(
message,
std::make_unique<HistoryView::GiftThemeBox>(message, this));
std::make_unique<HistoryView::GiftServiceBox>(message, this));
} else if (const auto &unique = _data.unique) {
return std::make_unique<HistoryView::MediaGeneric>(
message,
@@ -140,6 +140,7 @@ enum class GiftType : uchar {
StarGift, // count - stars
ChatTheme,
BirthdaySuggest,
GiftOffer,
};
struct GiftCode {
+149 -14
View File
@@ -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<HistoryServiceSuggestFinish>();
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<HistoryMessageSuggestion>();
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<HistoryMessageReplyMarkup>();
markup->updateSuggestControls(actions);
}
}
} else if (type == mtpc_messageActionStarGiftPurchaseOfferDeclined) {
const auto &data = action.c_messageActionStarGiftPurchaseOfferDeclined();
UpdateComponents(HistoryServiceSuggestFinish::Bit());
const auto finish = Get<HistoryServiceSuggestFinish>();
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<HistoryMessageSuggestion>()) {
Assert(suggestion->gift != nullptr);
_media = std::make_unique<Data::MediaGiftBox>(
this,
_from,
Data::GiftCode{
.unique = suggestion->gift,
.type = Data::GiftType::GiftOffer,
});
}
}, [](const auto &) {
});
}
+2 -1
View File
@@ -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);
@@ -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]
@@ -62,6 +62,7 @@ enum class SuggestionActions : uchar {
None,
Decline,
AcceptAndDecline,
GiftOfferActions,
};
struct HistoryMessageVia : RuntimeComponent<HistoryMessageVia, HistoryItem> {
@@ -624,12 +625,12 @@ struct HistoryMessageFactcheck
struct HistoryMessageSuggestion
: RuntimeComponent<HistoryMessageSuggestion, HistoryItem> {
std::shared_ptr<Data::UniqueGift> 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<HistoryServiceSuggestFinish, HistoryItem>
, HistoryServiceDependentData {
CreditsAmount successPrice;
CreditsAmount price;
SuggestRefundType refundType = SuggestRefundType::None;
};
@@ -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,
@@ -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<void()> 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<const HistoryItem*> 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<BubbleRoundingKey, CachedBg> _cachedBg;
mutable base::flat_map<BubbleRoundingKey, QPainterPath> _cachedOutline;
mutable std::unique_ptr<Ui::GlareEffect> _glare;
Fn<void()> _repaint;
};
KeyboardStyle::KeyboardStyle(
const style::BotKeyboardButton &st,
Fn<void()> 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<const HistoryItem*> 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<Ui::GlareEffect>();
_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<HistoryItem*> 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<ReplyKeyboard>(
item,
std::make_unique<KeyboardStyle>(
st::msgBotKbButton,
[=] { item->history()->owner().requestItemRepaint(item); }));
}
void Element::previousInBlocksChanged() {
recountThreadBarInBlocks();
recountDisplayDateInBlocks();
@@ -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();
@@ -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<void()> 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<const HistoryItem*> 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<BubbleRoundingKey, QImage> _cachedBg;
mutable base::flat_map<BubbleRoundingKey, QPainterPath> _cachedOutline;
mutable std::unique_ptr<Ui::GlareEffect> _glare;
Fn<void()> _repaint;
rpl::lifetime _lifetime;
};
KeyboardStyle::KeyboardStyle(
const style::BotKeyboardButton &st,
Fn<void()> 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<const HistoryItem*> 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<Ui::GlareEffect>();
_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<ReplyKeyboard>(
item,
std::make_unique<KeyboardStyle>(
st::msgBotKbButton,
[=] { item->history()->owner().requestItemRepaint(item); }));
}
void Message::validateFromNameText(PeerData *from) const {
if (!from) {
if (_fromNameStatus) {
@@ -292,7 +292,6 @@ private:
void refreshInfoSkipBlock(HistoryItem *textItem);
[[nodiscard]] int monospaceMaxWidth() const;
void validateInlineKeyboard(HistoryMessageReplyMarkup *markup);
void updateViewButtonExistence();
[[nodiscard]] int viewButtonHeight() const;
@@ -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<HistoryMessageReplyMarkup>()) {
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<ServicePreMessage>()) {
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) {
@@ -33,6 +33,10 @@ public:
not_null<HistoryItem*> data,
Element *replacing);
void clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) override;
int marginTop() const override;
int marginBottom() const override;
bool isHidden() const override;
@@ -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<QColor>();
}
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;
}
@@ -36,6 +36,12 @@ public:
return top();
}
[[nodiscard]] virtual rpl::producer<QString> button() = 0;
// For now only subtitle() changes are observed.
[[nodiscard]] virtual rpl::producer<> changes() {
return nullptr;
}
[[nodiscard]] virtual auto buttonMinistars()
-> std::optional<Ui::Premium::MiniStarsType> {
return std::nullopt;
@@ -105,6 +111,8 @@ private:
[[nodiscard]] QRect buttonRect() const;
[[nodiscard]] QRect contentRect() const;
void applyContentChanges();
const not_null<Element*> _parent;
const std::unique_ptr<ServiceBoxContent> _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;
};
@@ -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<Element*> parent,
not_null<Data::MediaGiftBox*> 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<HistoryMessageSuggestion>()) {
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<QString> GiftThemeBox::button() {
void GiftServiceBox::checkKeyboardRemoval(
not_null<const HistoryMessageSuggestion*> suggestion,
bool expired) {
Expects(_data.type == Data::GiftType::GiftOffer);
const auto item = _parent->data();
if (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {
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<HistoryMessageReplyMarkup>()) {
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<QString> 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<UrlClickHandler>(
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<StickerPlayer> GiftThemeBox::stickerTakePlayer(
std::unique_ptr<StickerPlayer> GiftServiceBox::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return _sticker
@@ -752,17 +866,17 @@ std::unique_ptr<StickerPlayer> 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;
}
@@ -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<Element*> parent,
not_null<Data::MediaGiftBox*> 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<StickerPlayer> stickerTakePlayer(
@@ -163,6 +168,10 @@ public:
private:
void ensureStickerCreated() const;
void cacheUniqueBackground(int width, int height);
void checkKeyboardRemoval(
not_null<const HistoryMessageSuggestion*> suggestion,
bool expired);
void clearKeyboard();
const not_null<Element*> _parent;
const Data::GiftCode &_data;
@@ -171,6 +180,8 @@ private:
base::flat_map<float64, QImage> _patternCache;
bool _backroundPatterned = false;
mutable std::optional<Sticker> _sticker;
rpl::event_stream<> _changes;
base::Timer _changeTimer;
};