From 5f8da27c86b734a83531871af0ab01132fcebc58 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 19 Jun 2024 13:39:02 +0400 Subject: [PATCH] Display nice price tag, handle pay in albums. --- Telegram/SourceFiles/apiwrap.cpp | 76 ++++++++- Telegram/SourceFiles/apiwrap.h | 6 + Telegram/SourceFiles/boxes/send_files_box.cpp | 10 +- .../chat_helpers/message_field.cpp | 4 +- .../history/view/media/history_view_media.h | 14 ++ .../view/media/history_view_media_grouped.cpp | 53 ++++++ .../view/media/history_view_media_grouped.h | 3 + .../history/view/media/history_view_photo.cpp | 154 +++++++++++++----- .../history/view/media/history_view_photo.h | 14 +- 9 files changed, 288 insertions(+), 46 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 424c8ccc1..86481cec7 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -4212,6 +4212,67 @@ void ApiWrap::sendMediaWithRandomId( }); } +void ApiWrap::sendMultiPaidMedia( + not_null item, + const QVector &medias, + Api::SendOptions options, + uint64 randomId, + Fn done) { + Expects(options.price > 0); + + const auto history = item->history(); + const auto replyTo = item->replyTo(); + + auto caption = item->originalText(); + TextUtilities::Trim(caption); + auto sentEntities = Api::EntitiesToMTP( + _session, + caption.entities, + Api::ConvertOption::SkipLocal); + + using Flag = MTPmessages_SendMedia::Flag; + const auto flags = Flag(0) + | (replyTo ? Flag::f_reply_to : Flag(0)) + | (ShouldSendSilent(history->peer, options) + ? Flag::f_silent + : Flag(0)) + | (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0)) + | (options.scheduled ? Flag::f_schedule_date : Flag(0)) + | (options.sendAs ? Flag::f_send_as : Flag(0)) + | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)) + | (options.effectId ? Flag::f_effect : Flag(0)) + | (options.invertCaption ? Flag::f_invert_media : Flag(0)); + + auto &histories = history->owner().histories(); + const auto peer = history->peer; + const auto itemId = item->fullId(); + histories.sendPreparedMessage( + history, + replyTo, + randomId, + Data::Histories::PrepareMessage( + MTP_flags(flags), + peer->input, + Data::Histories::ReplyToPlaceholder(), + MTP_inputMediaPaidMedia( + MTP_long(options.price), + MTP_vector(medias)), + MTP_string(caption.text), + MTP_long(randomId), + MTPReplyMarkup(), + sentEntities, + MTP_int(options.scheduled), + (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), + Data::ShortcutIdToMTP(_session, options.shortcutId), + MTP_long(options.effectId) + ), [=](const MTPUpdates &result, const MTP::Response &response) { + if (done) done(true); + }, [=](const MTP::Error &error, const MTP::Response &response) { + if (done) done(false); + sendMessageFail(error, peer, randomId, itemId); + }); +} + void ApiWrap::sendAlbumWithUploaded( not_null item, const MessageGroupId &groupId, @@ -4266,7 +4327,7 @@ void ApiWrap::sendAlbumIfReady(not_null album) { _sendingAlbums.remove(groupId); return; } else if (medias.size() < 2) { - const auto &single = medias.front().c_inputSingleMedia(); + const auto &single = medias.front().data(); sendMediaWithRandomId( sample, single.vmedia(), @@ -4274,6 +4335,19 @@ void ApiWrap::sendAlbumIfReady(not_null album) { single.vrandom_id().v); _sendingAlbums.remove(groupId); return; + } else if (album->options.price > 0) { + const auto &single = medias.front().data(); + auto list = medias | ranges::view::transform([]( + const MTPInputSingleMedia &media) { + return MTPInputMedia(media.data().vmedia()); + }) | ranges::to>(); + sendMultiPaidMedia( + sample, + std::move(list), + album->options, + single.vrandom_id().v); + _sendingAlbums.remove(groupId); + return; } const auto history = sample->history(); const auto replyTo = sample->replyTo(); diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 9710ee1bd..3904eb3d3 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -545,6 +545,12 @@ private: Api::SendOptions options, uint64 randomId, Fn done = nullptr); + void sendMultiPaidMedia( + not_null item, + const QVector &medias, + Api::SendOptions options, + uint64 randomId, + Fn done = nullptr); void getTopPromotionDelayed(TimeId now, TimeId next); void topPromotionDone(const MTPhelp_PromoData &proxy); diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index f80279345..ac27ddb0f 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -680,9 +680,11 @@ bool SendFilesBox::hasSpoilerMenu() const { bool SendFilesBox::canChangePrice() const { const auto way = _sendWay.current(); - return _list.canChangePrice( - way.groupFiles() && way.sendImagesAsPhotos(), - way.sendImagesAsPhotos()); + return _captionToPeer + && _captionToPeer->isBroadcast() + && _list.canChangePrice( + way.groupFiles() && way.sendImagesAsPhotos(), + way.sendImagesAsPhotos()); } void SendFilesBox::applyBlockChanges() { @@ -754,6 +756,7 @@ void SendFilesBox::refreshPriceTag() { raw, tr::lng_paid_price(lt_price, std::move(price)), st::paidTagLabel); + label->show(); label->sizeValue() | rpl::start_with_next([=](QSize size) { const auto inner = QRect(QPoint(), size); const auto rect = inner.marginsAdded(st::paidTagPadding); @@ -833,6 +836,7 @@ void SendFilesBox::initSendWay() { block.setSendWay(value); } refreshButtons(); + refreshPriceTag(); if (was != hidden()) { updateBoxSize(); updateControlsGeometry(); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 38435758e..5d8be6144 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -1186,7 +1186,9 @@ base::unique_qptr PremiumRequiredSendRestriction( const auto margins = (st.textMargins + st.placeholderMargins); const auto available = width - margins.left() - margins.right(); label->resizeToWidth(available); - label->moveToLeft(margins.left(), margins.top(), width); + const auto height = label->height() + link->height(); + const auto top = (raw->height() - height) / 2; + label->moveToLeft(margins.left(), top, width); link->move( (width - link->width()) / 2, label->y() + label->height()); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 4926ca7c8..9d577255c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -223,6 +223,20 @@ public: QPoint point, StateRequest request) const; + virtual void drawPriceTag( + Painter &p, + QRect rthumb, + const PaintContext &context, + Fn generateBackground) const { + Unexpected("Price tag method call."); + } + [[nodiscard]] virtual ClickHandlerPtr priceTagLink() const { + Unexpected("Price tag method call."); + } + [[nodiscard]] virtual QImage priceTagBackground() const { + Unexpected("Price tag method call."); + } + [[nodiscard]] virtual bool animating() const { return false; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp index 6a67bf426..9f93e7203 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "storage/storage_shared_media.h" #include "lang/lang_keys.h" +#include "media/streaming/media_streaming_utility.h" #include "ui/grouped_layout.h" #include "ui/chat/chat_style.h" #include "ui/chat/message_bubble.h" @@ -290,6 +291,42 @@ QMargins GroupedMedia::groupedPadding() const { (normal.bottom() - grouped.bottom()) + addToBottom); } +Media *GroupedMedia::lookupUnpaidMedia() const { + if (_parts.empty()) { + return nullptr; + } + const auto media = _parts.front().content.get(); + const auto photo = media ? media->getPhoto() : nullptr; + return (photo && photo->extendedMediaPreview()) ? media : nullptr; +} + +QImage GroupedMedia::generatePriceTagBackground(QRect full) const { + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + full.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + auto p = QPainter(&result); + const auto shift = -full.topLeft(); + const auto skip1 = st::historyGroupSkip / 2; + const auto skip2 = st::historyGroupSkip - skip1; + for (const auto &part : _parts) { + auto background = part.content->priceTagBackground(); + const auto extended = part.geometry.translated(shift).marginsAdded( + { skip1, skip1, skip2, skip2 }); + if (background.isNull()) { + p.fillRect(extended, Qt::black); + } else { + p.drawImage(extended, background); + } + } + p.end(); + + return ::Media::Streaming::PrepareBlurredBackground( + full.size(), + std::move(result)); +} + void GroupedMedia::drawHighlight( Painter &p, const PaintContext &context, @@ -351,6 +388,8 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const { ? Ui::BubbleRounding{ kSmall, kSmall, kSmall, kSmall } : adjustedBubbleRounding(); auto highlight = context.highlight.range; + const auto unpaid = lookupUnpaidMedia(); + auto fullRect = QRect(); const auto subpartHighlight = IsSubGroupSelection(highlight); for (auto i = 0, count = int(_parts.size()); i != count; ++i) { const auto &part = _parts[i]; @@ -390,11 +429,20 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const { if (!part.cache.isNull()) { nowCache = true; } + if (unpaid) { + fullRect = fullRect.united(part.geometry); + } } if (nowCache && !wasCache) { history()->owner().registerHeavyViewPart(_parent); } + if (unpaid) { + unpaid->drawPriceTag(p, fullRect, context, [&] { + return generatePriceTagBackground(fullRect); + }); + } + // date if (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) { auto fullRight = width(); @@ -455,6 +503,11 @@ PointState GroupedMedia::pointState(QPoint point) const { TextState GroupedMedia::textState(QPoint point, StateRequest request) const { const auto groupPadding = groupedPadding(); auto result = getPartState(point - QPoint(0, groupPadding.top()), request); + if (const auto unpaid = lookupUnpaidMedia()) { + if (QRect(0, 0, width(), height()).contains(point)) { + result.link = unpaid->priceTagLink(); + } + } if (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) { auto fullRight = width(); auto fullBottom = height(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h index 84dca4884..b101fef91 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h @@ -149,6 +149,9 @@ private: RectParts sides) const; [[nodiscard]] QMargins groupedPadding() const; + [[nodiscard]] Media *lookupUnpaidMedia() const; + [[nodiscard]] QImage generatePriceTagBackground(QRect full) const; + mutable std::optional _captionItem; std::vector _parts; Mode _mode = Mode::Grid; diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 7650fcd9a..e3f6c91c5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -60,6 +60,14 @@ struct Photo::Streamed { QImage roundingMask; }; +struct Photo::PriceTag { + uint64 price = 0; + QImage cache; + QColor darken; + QColor fg; + ClickHandlerPtr link; +}; + Photo::Streamed::Streamed( std::shared_ptr<::Media::Streaming::Document> shared) : instance(std::move(shared), nullptr) { @@ -281,8 +289,8 @@ void Photo::draw(Painter &p, const PaintContext &context) const { const auto st = context.st; const auto sti = context.imageStyle(); const auto preview = _data->extendedMediaPreview(); - auto loaded = preview || _dataMedia->loaded(); - auto displayLoading = !preview && _data->displayLoading(); + const auto loaded = preview || _dataMedia->loaded(); + const auto displayLoading = !preview && _data->displayLoading(); auto inWebPage = (_parent->media() != this); auto paintx = 0, painty = 0, paintw = width(), painth = height(); @@ -370,7 +378,9 @@ void Photo::draw(Painter &p, const PaintContext &context) const { _animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg); } } else if (preview) { - paintPriceTag(p, rthumb); + drawPriceTag(p, rthumb, context, [&] { + return _spoiler ? _spoiler->background : QImage(); + }); } if (showEnlarge) { auto hq = PainterHighQualityEnabler(p); @@ -403,41 +413,93 @@ void Photo::draw(Painter &p, const PaintContext &context) const { } } -void Photo::paintPriceTag(Painter &p, QRect rthumb) const { +void Photo::setupPriceTag() const { const auto media = parent()->data()->media(); const auto invoice = media ? media->invoice() : nullptr; const auto price = invoice->isPaidMedia ? invoice->amount : 0; if (!price) { return; } + _priceTag = std::make_unique(); + _priceTag->price = price; +} - auto text = Ui::Text::String(); - text.setText( - st::semiboldTextStyle, - tr::lng_paid_price( - tr::now, - lt_price, - QChar(0x2B50) + Lang::FormatCountDecimal(invoice->amount))); - const auto width = text.maxWidth(); - const auto inner = QRect(0, 0, width, text.minHeight()); - const auto outer = inner.marginsAdded(st::paidTagPadding); - const auto size = outer.size(); +void Photo::drawPriceTag( + Painter &p, + QRect rthumb, + const PaintContext &context, + Fn generateBackground) const { + if (!_priceTag) { + setupPriceTag(); + if (!_priceTag) { + return; + } + } + const auto st = context.st; + const auto darken = st->msgDateImgBg()->c; + const auto fg = st->msgDateImgFg()->c; + if (_priceTag->cache.isNull() + || _priceTag->darken != darken + || _priceTag->fg != fg) { + auto bg = generateBackground(); + if (bg.isNull()) { + bg = QImage(2, 2, QImage::Format_ARGB32_Premultiplied); + bg.fill(Qt::black); + } - auto hq = PainterHighQualityEnabler(p); - p.setBrush(st::toastBg); - p.setPen(Qt::NoPen); + auto text = Ui::Text::String(); + text.setText( + st::semiboldTextStyle, + tr::lng_paid_price( + tr::now, + lt_price, + QChar(0x2B50) + Lang::FormatCountDecimal(_priceTag->price))); + const auto width = text.maxWidth(); + const auto inner = QRect(0, 0, width, text.minHeight()); + const auto outer = inner.marginsAdded(st::paidTagPadding); + const auto size = outer.size(); + const auto radius = std::min(size.width(), size.height()) / 2; + const auto ratio = style::DevicePixelRatio(); + auto cache = QImage( + size * ratio, + QImage::Format_ARGB32_Premultiplied); + cache.setDevicePixelRatio(ratio); + cache.fill(Qt::black); + auto p = Painter(&cache); + auto hq = PainterHighQualityEnabler(p); + p.drawImage( + QRect( + (size.width() - rthumb.width()) / 2, + (size.height() - rthumb.height()) / 2, + rthumb.width(), + rthumb.height()), + bg); + p.fillRect(QRect(QPoint(), size), darken); + p.setPen(fg); + text.draw(p, -outer.x(), -outer.y(), width); + p.end(); - const auto radius = std::min(size.width(), size.height()) / 2.; - const auto rect = QRect( - rthumb.x() + (rthumb.width() - size.width()) / 2, - rthumb.y() + (rthumb.height() - size.height()) / 2, - size.width(), - size.height()); - - p.drawRoundedRect(rect, radius, radius); - p.setPen(st::toastFg); - - text.draw(p, rect.x() - outer.x(), rect.y() - outer.y(), width); + _priceTag->darken = darken; + _priceTag->fg = fg; + _priceTag->cache = Images::Round( + std::move(cache), + Images::CornersMask(radius)); + } + const auto &cache = _priceTag->cache; + const auto size = cache.size() / cache.devicePixelRatio(); + const auto left = rthumb.x() + (rthumb.width() - size.width()) / 2; + const auto top = rthumb.y() + (rthumb.height() - size.height()) / 2; + p.drawImage(left, top, cache); + if (context.selected()) { + auto hq = PainterHighQualityEnabler(p); + const auto radius = std::min(size.width(), size.height()) / 2; + p.setPen(Qt::NoPen); + p.setBrush(st->msgSelectOverlay()); + p.drawRoundedRect( + QRect(left, top, size.width(), size.height()), + radius, + radius); + } } void Photo::validateUserpicImageCache(QSize size, bool forum) const { @@ -647,12 +709,24 @@ QRect Photo::enlargeRect() const { }; } -ClickHandlerPtr Photo::ensureExtendedMediaLink() const { +ClickHandlerPtr Photo::priceTagLink() const { const auto item = parent()->data(); - if (!_extendedMediaLink && item->isRegular()) { - _extendedMediaLink = MakePaidMediaLink(item); + if (!item->isRegular()) { + return nullptr; + } else if (!_priceTag) { + setupPriceTag(); + if (!_priceTag) { + return nullptr; + } } - return _extendedMediaLink; + if (!_priceTag->link) { + _priceTag->link = MakePaidMediaLink(item); + } + return _priceTag->link; +} + +QImage Photo::priceTagBackground() const { + return _spoiler ? _spoiler->background : QImage(); } TextState Photo::textState(QPoint point, StateRequest request) const { @@ -669,7 +743,7 @@ TextState Photo::textState(QPoint point, StateRequest request) const { if (QRect(paintx, painty, paintw, painth).contains(point)) { ensureDataMediaCreated(); result.link = _data->extendedMediaPreview() - ? ensureExtendedMediaLink() + ? priceTagLink() : (_spoiler && !_spoiler->revealed) ? _spoiler->link : _data->uploading() @@ -735,8 +809,9 @@ void Photo::drawGrouped( const auto st = context.st; const auto sti = context.imageStyle(); - const auto loaded = _dataMedia->loaded(); - const auto displayLoading = _data->displayLoading(); + const auto preview = _data->extendedMediaPreview(); + const auto loaded = preview || _dataMedia->loaded(); + const auto displayLoading = !preview && _data->displayLoading(); if (displayLoading) { ensureAnimation(); @@ -841,7 +916,9 @@ TextState Photo::getStateGrouped( return {}; } ensureDataMediaCreated(); - return TextState(_parent, (_spoiler && !_spoiler->revealed) + return TextState(_parent, _data->extendedMediaPreview() + ? priceTagLink() + : (_spoiler && !_spoiler->revealed) ? _spoiler->link : _data->uploading() ? _cancell @@ -887,7 +964,8 @@ void Photo::validateGroupedCache( ensureDataMediaCreated(); - const auto loaded = _dataMedia->loaded(); + const auto preview = _data->extendedMediaPreview(); + const auto loaded = preview || _dataMedia->loaded(); const auto loadLevel = loaded ? 2 : (_dataMedia->thumbnailInline() diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index ad35e6481..223eae37e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -75,6 +75,14 @@ public: QPoint point, StateRequest request) const override; + void drawPriceTag( + Painter &p, + QRect rthumb, + const PaintContext &context, + Fn generateBackground) const override; + ClickHandlerPtr priceTagLink() const override; + QImage priceTagBackground() const override; + void hideSpoilers() override; bool needsBubble() const override; bool customInfoLayout() const override { @@ -97,6 +105,7 @@ protected: private: struct Streamed; + struct PriceTag; void create(FullMsgId contextId, PeerData *chat = nullptr); @@ -106,6 +115,7 @@ private: void ensureDataMediaCreated() const; void dataMediaCreated() const; + void setupPriceTag() const; QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; @@ -147,13 +157,11 @@ private: [[nodiscard]] QSize photoSize() const; [[nodiscard]] QRect enlargeRect() const; - void paintPriceTag(Painter &p, QRect rthumb) const; - [[nodiscard]] ClickHandlerPtr ensureExtendedMediaLink() const; void togglePollingStory(bool enabled) const; const not_null _data; const FullStoryId _storyId; - mutable ClickHandlerPtr _extendedMediaLink; + mutable std::unique_ptr _priceTag; mutable std::shared_ptr _dataMedia; mutable std::unique_ptr _streamed; const std::unique_ptr _spoiler;