diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 0043c59b3..f4d2c145d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3311,6 +3311,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_spoiler_effect" = "Hide with Spoiler"; "lng_context_disable_spoiler" = "Remove Spoiler"; +"lng_context_make_paid" = "Make This Content Paid"; +"lng_context_change_price" = "Change Price"; "lng_factcheck_title" = "Fact Check"; "lng_factcheck_placeholder" = "Add Facts or Context"; @@ -3322,6 +3324,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_factcheck_bottom" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country ({country}) responsible for combatting misinformation."; "lng_factcheck_links" = "Only **t.me/** links are allowed."; +"lng_paid_title" = "Paid Content"; +"lng_paid_enter_cost" = "Enter Unlock Cost"; +"lng_paid_cost_placeholder" = "Stars to Unlock"; +"lng_paid_about" = "Users will have to transfer this amount of Stars to your channel in order to view this media. {link}"; +"lng_paid_about_link" = "More about stars >"; +"lng_paid_price" = "Unlock for {price}"; + "lng_translate_show_original" = "Show Original"; "lng_translate_bar_to" = "Translate to {name}"; "lng_translate_bar_to_other" = "Translate to {name}"; diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index efd92a9bc..cd8aa54e2 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -20,6 +20,7 @@ namespace Api { inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); struct SendOptions { + uint64 price = 0; PeerData *sendAs = nullptr; TimeId scheduled = 0; BusinessShortcutId shortcutId = 0; diff --git a/Telegram/SourceFiles/api/api_views.cpp b/Telegram/SourceFiles/api/api_views.cpp index 6c8ec8df7..e80f56e21 100644 --- a/Telegram/SourceFiles/api/api_views.cpp +++ b/Telegram/SourceFiles/api/api_views.cpp @@ -55,7 +55,9 @@ void ViewsManager::removeIncremented(not_null peer) { _incremented.remove(peer); } -void ViewsManager::pollExtendedMedia(not_null item) { +void ViewsManager::pollExtendedMedia( + not_null item, + bool force) { if (!item->isRegular()) { return; } @@ -63,14 +65,20 @@ void ViewsManager::pollExtendedMedia(not_null item) { const auto peer = item->history()->peer; auto &request = _pollRequests[peer]; if (request.ids.contains(id) || request.sent.contains(id)) { - return; + if (!force || request.forced) { + return; + } } request.ids.emplace(id); - if (!request.id && !request.when) { - request.when = crl::now() + kPollExtendedMediaPeriod; + if (force) { + request.forced = true; } - if (!_pollTimer.isActive()) { - _pollTimer.callOnce(kPollExtendedMediaPeriod); + const auto delay = force ? 1 : kPollExtendedMediaPeriod; + if (!request.id && (!request.when || force)) { + request.when = crl::now() + delay; + } + if (!_pollTimer.isActive() || force) { + _pollTimer.callOnce(delay); } } @@ -160,9 +168,12 @@ void ViewsManager::sendPollRequests( if (i->second.ids.empty()) { i = _pollRequests.erase(i); } else { - i->second.when = now + kPollExtendedMediaPeriod; - if (!_pollTimer.isActive()) { - _pollTimer.callOnce(kPollExtendedMediaPeriod); + const auto delay = i->second.forced + ? 1 + : kPollExtendedMediaPeriod; + i->second.when = now + delay; + if (!_pollTimer.isActive() || i->second.forced) { + _pollTimer.callOnce(delay); } ++i; } diff --git a/Telegram/SourceFiles/api/api_views.h b/Telegram/SourceFiles/api/api_views.h index 759d3c6a8..f8d19d15b 100644 --- a/Telegram/SourceFiles/api/api_views.h +++ b/Telegram/SourceFiles/api/api_views.h @@ -26,7 +26,7 @@ public: void scheduleIncrement(not_null item); void removeIncremented(not_null peer); - void pollExtendedMedia(not_null item); + void pollExtendedMedia(not_null item, bool force = false); private: struct PollExtendedMediaRequest { @@ -34,6 +34,7 @@ private: mtpRequestId id = 0; base::flat_set ids; base::flat_set sent; + bool forced = false; }; void viewsIncrement(); diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 62f2cfbd4..424c8ccc1 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -4188,7 +4188,11 @@ void ApiWrap::sendMediaWithRandomId( MTP_flags(flags), peer->input, Data::Histories::ReplyToPlaceholder(), - media, + (options.price + ? MTPInputMedia(MTP_inputMediaPaidMedia( + MTP_long(options.price), + MTP_vector(1, media))) + : media), MTP_string(caption.text), MTP_long(randomId), MTPReplyMarkup(), diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index d46121c2f..bd089a9b5 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -463,6 +463,7 @@ void EditCaptionBox::rebuildPreview() { st::defaultComposeControls, gifPaused, file, + [] { return true; }, Ui::AttachControls::Type::EditOnly); _isPhoto = (media && media->isPhoto()); const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType); diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index fbaaf561a..f80279345 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "storage/localstorage.h" #include "storage/storage_media_prepare.h" +#include "iv/iv_instance.h" #include "mainwidget.h" #include "main/main_session.h" #include "main/main_session_settings.h" @@ -36,9 +37,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/attach/attach_single_file_preview.h" #include "ui/chat/attach/attach_single_media_preview.h" #include "ui/grouped_layout.h" +#include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/controls/emoji_button.h" #include "ui/painter.h" +#include "ui/vertical_list.h" #include "lottie/lottie_single_player.h" #include "data/data_document.h" #include "data/data_user.h" @@ -58,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { constexpr auto kMaxMessageLength = 4096; +constexpr auto kMaxPrice = 1000ULL; using Ui::SendFilesWay; @@ -103,6 +107,74 @@ rpl::producer FieldPlaceholder( : tr::lng_photos_comment(); } +void EditPriceBox( + not_null box, + not_null session, + uint64 price, + Fn apply) { + const auto owner = &session->data(); + box->setTitle(tr::lng_paid_title()); + AddSubsectionTitle( + box->verticalLayout(), + tr::lng_paid_enter_cost(), + (st::boxRowPadding - QMargins( + st::defaultSubsectionTitlePadding.left(), + 0, + st::defaultSubsectionTitlePadding.right(), + 0))); + const auto field = box->addRow(object_ptr( + box, + st::editTagField, + tr::lng_paid_cost_placeholder(), + price ? QString::number(price) : QString())); + field->selectAll(); + field->setMaxLength(QString::number(kMaxPrice).size()); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + const auto about = box->addRow( + object_ptr( + box, + tr::lng_paid_about( + lt_link, + tr::lng_paid_about_link() | Ui::Text::ToLink(), + Ui::Text::WithEntities), + st::paidAmountAbout), + st::boxRowPadding + QMargins(0, st::sendMediaRowSkip, 0, 0)); + about->setClickHandlerFilter([=](const auto &...) { + Core::App().iv().openWithIvPreferred( + session, + u"https://telegram.org/blog/telegram-stars"_q); + return false; + }); + + field->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(field); + st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); + }, field->lifetime()); + + const auto save = [=] { + const auto now = field->getLastText().toULongLong(); + if (now > kMaxPrice) { + field->showError(); + return; + } + const auto weak = Ui::MakeWeak(box); + apply(now); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + + field->submits( + ) | rpl::start_with_next(save, field->lifetime()); + + box->addButton(tr::lng_settings_save(), save); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + } // namespace SendFilesLimits DefaultLimitsForPeer(not_null peer) { @@ -153,7 +225,8 @@ SendFilesBox::Block::Block( int from, int till, Fn gifPaused, - SendFilesWay way) + SendFilesWay way, + Fn canToggleSpoiler) : _items(items) , _from(from) , _till(till) { @@ -170,14 +243,16 @@ SendFilesBox::Block::Block( parent.get(), st, my, - way); + way, + std::move(canToggleSpoiler)); _preview.reset(preview); } else { const auto media = Ui::SingleMediaPreview::Create( parent, st, gifPaused, - first); + first, + std::move(canToggleSpoiler)); if (media) { _isSingleMedia = true; _preview.reset(media); @@ -385,6 +460,9 @@ Fn SendFilesBox::prepareSendMenuDetails( : _invertCaption ? SendMenu::CaptionState::Above : SendMenu::CaptionState::Below; + result.price = canChangePrice() + ? _price.current() + : std::optional(); return result; }); } @@ -398,6 +476,7 @@ auto SendFilesBox::prepareSendMenuCallback() case Type::CaptionUp: _invertCaption = true; break; case Type::SpoilerOn: toggleSpoilers(true); break; case Type::SpoilerOff: toggleSpoilers(false); break; + case Type::ChangePrice: changePrice(); break; default: SendMenu::DefaultCallback( _show, @@ -588,14 +667,22 @@ void SendFilesBox::refreshButtons() { addMenuButton(); } -bool SendFilesBox::hasSendMenu(const SendMenu::Details &details) const { +bool SendFilesBox::hasSendMenu(const MenuDetails &details) const { return (details.type != SendMenu::Type::Disabled) || (details.spoiler != SendMenu::SpoilerState::None) || (details.caption != SendMenu::CaptionState::None); } bool SendFilesBox::hasSpoilerMenu() const { - return _list.hasSpoilerMenu(_sendWay.current().sendImagesAsPhotos()); + return !hasPrice() + && _list.hasSpoilerMenu(_sendWay.current().sendImagesAsPhotos()); +} + +bool SendFilesBox::canChangePrice() const { + const auto way = _sendWay.current(); + return _list.canChangePrice( + way.groupFiles() && way.sendImagesAsPhotos(), + way.sendImagesAsPhotos()); } void SendFilesBox::applyBlockChanges() { @@ -618,6 +705,71 @@ void SendFilesBox::toggleSpoilers(bool enabled) { } } +void SendFilesBox::changePrice() { + const auto weak = Ui::MakeWeak(this); + const auto session = &_show->session(); + const auto now = _price.current(); + _show->show(Box(EditPriceBox, session, now, [=](uint64 price) { + if (weak && price != now) { + _price = price; + refreshPriceTag(); + } + })); +} + +bool SendFilesBox::hasPrice() const { + return canChangePrice() && _price.current() > 0; +} + +void SendFilesBox::refreshPriceTag() { + const auto resetSpoilers = hasPrice() || _priceTag; + if (resetSpoilers) { + for (auto &file : _list.files) { + file.spoiler = false; + } + for (auto &block : _blocks) { + block.toggleSpoilers(hasPrice()); + } + } + if (!hasPrice()) { + _priceTag = nullptr; + } else if (!_priceTag) { + _priceTag = std::make_unique(_inner.data()); + const auto raw = _priceTag.get(); + + raw->show(); + raw->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::toastBg); + p.setPen(Qt::NoPen); + const auto radius = std::min(raw->width(), raw->height()) / 2.; + p.drawRoundedRect(raw->rect(), radius, radius); + }, raw->lifetime()); + + auto price = _price.value() | rpl::map([=](uint64 amount) { + return QChar(0x2B50) + Lang::FormatCountDecimal(amount); + }); + const auto label = Ui::CreateChild( + raw, + tr::lng_paid_price(lt_price, std::move(price)), + st::paidTagLabel); + label->sizeValue() | rpl::start_with_next([=](QSize size) { + const auto inner = QRect(QPoint(), size); + const auto rect = inner.marginsAdded(st::paidTagPadding); + raw->resize(rect.size()); + label->move(-rect.topLeft()); + }, label->lifetime()); + _inner->sizeValue() | rpl::start_with_next([=](QSize size) { + raw->move( + (size.width() - raw->width()) / 2, + (size.height() - raw->height()) / 2); + }, raw->lifetime()); + } else { + _priceTag->raise(); + } +} + void SendFilesBox::addMenuButton() { const auto details = _sendMenuDetails(); if (!hasSendMenu(details)) { @@ -766,7 +918,8 @@ void SendFilesBox::pushBlock(int from, int till) { from, till, gifPaused, - _sendWay.current()); + _sendWay.current(), + [=] { return !hasPrice(); }); auto &block = _blocks.back(); const auto widget = _inner->add( block.takeWidget(), @@ -893,6 +1046,7 @@ void SendFilesBox::pushBlock(int from, int till) { void SendFilesBox::refreshControls(bool initial) { refreshButtons(); + refreshPriceTag(); refreshTitleText(); updateSendWayControls(); updateCaptionPlaceholder(); @@ -1447,6 +1601,7 @@ void SendFilesBox::send( auto child = _sendMenuDetails(); child.spoiler = SendMenu::SpoilerState::None; child.caption = SendMenu::CaptionState::None; + child.price = std::nullopt; return SendMenu::DefaultCallback(_show, sendCallback())( { .type = SendMenu::ActionType::Schedule }, child); @@ -1475,6 +1630,7 @@ void SendFilesBox::send( ? _caption->getTextWithAppliedMarkdown() : TextWithTags(); options.invertCaption = _invertCaption; + options.price = hasPrice() ? _price.current() : 0; if (!validateLength(caption.text)) { return; } diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 1e95a1aa8..f16dffb8f 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -149,7 +149,8 @@ private: int from, int till, Fn gifPaused, - Ui::SendFilesWay way); + Ui::SendFilesWay way, + Fn canToggleSpoiler); Block(Block &&other) = default; Block &operator=(Block &&other) = default; @@ -190,6 +191,11 @@ private: void addMenuButton(); void applyBlockChanges(); void toggleSpoilers(bool enabled); + void changePrice(); + + [[nodiscard]] bool canChangePrice() const; + [[nodiscard]] bool hasPrice() const; + void refreshPriceTag(); bool validateLength(const QString &text) const; void refreshButtons(); @@ -251,6 +257,8 @@ private: SendFilesCheck _check; SendFilesConfirmed _confirmedCallback; Fn _cancelledCallback; + rpl::variable _price = 0; + std::unique_ptr _priceTag; bool _confirmed = false; bool _invertCaption = false; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 6b69aa136..b268377f9 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -71,6 +71,7 @@ ComposeIcons { menuSpoilerOff: icon; menuBelow: icon; menuAbove: icon; + menuPrice: icon; stripBubble: icon; stripExpandPanel: icon; @@ -610,6 +611,7 @@ defaultComposeIcons: ComposeIcons { menuSpoilerOff: menuIconSpoilerOff; menuBelow: menuIconBelow; menuAbove: menuIconAbove; + menuPrice: menuIconEarn; stripBubble: icon{ { "chat/reactions_bubble_shadow", windowShadowFg }, @@ -1406,3 +1408,15 @@ editTagField: InputField(defaultInputField) { editTagLimit: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } + +paidStarIcon: icon {{ "settings/premium/star", creditsBg1 }}; +paidStarIconTop: 7px; +paidAmountAbout: FlatLabel(defaultFlatLabel) { + minWidth: 256px; + textFg: windowSubTextFg; +} +paidTagLabel: FlatLabel(defaultFlatLabel) { + textFg: radialFg; + style: semiboldTextStyle; +} +paidTagPadding: margins(16px, 6px, 16px, 6px); diff --git a/Telegram/SourceFiles/data/data_cloud_file.cpp b/Telegram/SourceFiles/data/data_cloud_file.cpp index 526dd8904..7b3e73828 100644 --- a/Telegram/SourceFiles/data/data_cloud_file.cpp +++ b/Telegram/SourceFiles/data/data_cloud_file.cpp @@ -170,6 +170,12 @@ void UpdateCloudFile( if (data.progressivePartSize && !file.location.valid()) { file.progressivePartSize = data.progressivePartSize; } + if (data.location.width() + && data.location.height() + && !file.location.valid() + && !file.location.width()) { + file.location = data.location; + } return; } diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index e076438d7..1a8b052db 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -323,7 +323,7 @@ bool UpdateExtendedMedia( auto changed = false; const auto count = int(media.size()); for (auto i = 0; i != count; ++i) { - if (i < invoice.extendedMedia.size()) { + if (i <= invoice.extendedMedia.size()) { invoice.extendedMedia.emplace_back(); changed = true; } @@ -388,6 +388,7 @@ Invoice ComputeInvoiceData( auto result = Invoice{ .amount = data.vstars_amount().v, .currency = Ui::kCreditsCurrency, + .isPaidMedia = true, }; UpdateExtendedMedia(result, item, data.vextended_media().v); return result; @@ -1908,6 +1909,7 @@ MediaInvoice::MediaInvoice( .title = data.title, .description = data.description, .photo = data.photo, + .isPaidMedia = data.isPaidMedia, .isTest = data.isTest, } { _invoice.extendedMedia.reserve(data.extendedMedia.size()); diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 3e26877d4..62d03c53d 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -94,6 +94,7 @@ struct Invoice { TextWithEntities description; std::vector> extendedMedia; PhotoData *photo = nullptr; + bool isPaidMedia = false; bool isTest = false; }; [[nodiscard]] bool HasExtendedMedia(const Invoice &invoice); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp index 273262e8d..e5c01ba70 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp @@ -7,9 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/media/history_view_media_common.h" +#include "api/api_views.h" +#include "apiwrap.h" #include "ui/text/format_values.h" #include "ui/painter.h" +#include "core/click_handler_types.h" #include "data/data_document.h" +#include "data/data_session.h" #include "data/data_wall_paper.h" #include "data/data_media_types.h" #include "history/view/history_view_element.h" @@ -19,7 +23,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_document.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_theme_document.h" +#include "history/history_item.h" +#include "history/history.h" +#include "main/main_session.h" +#include "mainwindow.h" #include "media/streaming/media_streaming_utility.h" +#include "payments/payments_checkout_process.h" +#include "payments/payments_non_panel_process.h" +#include "window/window_session_controller.h" #include "styles/style_chat.h" namespace HistoryView { @@ -180,4 +191,34 @@ QSize CountPhotoMediaSize( media.scaled(media.width(), newWidth, Qt::KeepAspectRatio)); } +ClickHandlerPtr MakePaidMediaLink(not_null item) { + return std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + const auto controller = my.sessionWindow.get(); + const auto itemId = item->fullId(); + const auto session = &item->history()->session(); + using Result = Payments::CheckoutResult; + const auto done = crl::guard(session, [=](Result result) { + if (result != Result::Paid) { + return; + } else if (const auto item = session->data().message(itemId)) { + session->api().views().pollExtendedMedia(item, true); + } + }); + Payments::CheckoutProcess::Start( + item, + Payments::Mode::Payment, + (controller + ? crl::guard( + controller, + [=](auto) { controller->widget()->activate(); }) + : Fn()), + ((controller && Payments::IsCreditsInvoice(item)) + ? Payments::ProcessNonPanelPaymentFormFactory( + controller, + done) + : nullptr)); + }); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_common.h b/Telegram/SourceFiles/history/view/media/history_view_media_common.h index 2a46f5b3e..a37d0ab62 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_common.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_common.h @@ -75,4 +75,6 @@ void PaintInterpolatedIcon( int newWidth, int maxWidth); +[[nodiscard]] ClickHandlerPtr MakePaidMediaLink(not_null item); + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 01a41bdaa..7650fcd9a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_cursor_state.h" #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_spoiler.h" +#include "lang/lang_keys.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "media/streaming/media_streaming_document.h" @@ -38,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "core/application.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { namespace { @@ -140,7 +142,8 @@ void Photo::dataMediaCreated() const { if (_data->inlineThumbnailBytes().isEmpty() && !_dataMedia->image(PhotoSize::Large) - && !_dataMedia->image(PhotoSize::Thumbnail)) { + && !_dataMedia->image(PhotoSize::Thumbnail) + && !_data->extendedMediaPreview()) { _dataMedia->wanted(PhotoSize::Small, _realParent->fullId()); } history()->owner().registerHeavyViewPart(_parent); @@ -277,8 +280,9 @@ void Photo::draw(Painter &p, const PaintContext &context) const { _dataMedia->automaticLoad(_realParent->fullId(), _parent->data()); const auto st = context.st; const auto sti = context.imageStyle(); - auto loaded = _dataMedia->loaded(); - auto displayLoading = _data->displayLoading(); + const auto preview = _data->extendedMediaPreview(); + auto loaded = preview || _dataMedia->loaded(); + auto displayLoading = !preview && _data->displayLoading(); auto inWebPage = (_parent->media() != this); auto paintx = 0, painty = 0, paintw = width(), painth = height(); @@ -365,6 +369,8 @@ void Photo::draw(Painter &p, const PaintContext &context) const { QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); _animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg); } + } else if (preview) { + paintPriceTag(p, rthumb); } if (showEnlarge) { auto hq = PainterHighQualityEnabler(p); @@ -397,6 +403,43 @@ void Photo::draw(Painter &p, const PaintContext &context) const { } } +void Photo::paintPriceTag(Painter &p, QRect rthumb) 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; + } + + 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(); + + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::toastBg); + p.setPen(Qt::NoPen); + + 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); +} + void Photo::validateUserpicImageCache(QSize size, bool forum) const { const auto forumValue = forum ? 1 : 0; const auto large = _dataMedia->image(PhotoSize::Large); @@ -604,6 +647,14 @@ QRect Photo::enlargeRect() const { }; } +ClickHandlerPtr Photo::ensureExtendedMediaLink() const { + const auto item = parent()->data(); + if (!_extendedMediaLink && item->isRegular()) { + _extendedMediaLink = MakePaidMediaLink(item); + } + return _extendedMediaLink; +} + TextState Photo::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); @@ -617,7 +668,9 @@ TextState Photo::textState(QPoint point, StateRequest request) const { if (QRect(paintx, painty, paintw, painth).contains(point)) { ensureDataMediaCreated(); - result.link = (_spoiler && !_spoiler->revealed) + result.link = _data->extendedMediaPreview() + ? ensureExtendedMediaLink() + : (_spoiler && !_spoiler->revealed) ? _spoiler->link : _data->uploading() ? _cancell diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index 44fbed00c..ad35e6481 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -147,10 +147,13 @@ 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::shared_ptr _dataMedia; mutable std::unique_ptr _streamed; const std::unique_ptr _spoiler; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 99d9b4288..fe1a7b5b6 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -638,6 +638,7 @@ storiesEmojiPan: EmojiPan(defaultEmojiPan) { menuSpoilerOff: icon {{ "menu/spoiler_off", storiesComposeWhiteText }}; menuBelow: icon {{ "menu/link_below", storiesComposeWhiteText }}; menuAbove: icon {{ "menu/link_above", storiesComposeWhiteText }}; + menuPrice: icon {{ "menu/earn", storiesComposeWhiteText }}; stripBubble: icon{ { "chat/reactions_bubble_shadow", windowShadowFg }, diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index 58328da12..2ebfba197 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -678,6 +678,14 @@ FillMenuResult FillSendMenu( }, details); }, above ? &icons.menuBelow : &icons.menuAbove); } + if (details.price) { + menu->addAction( + ((*details.price > 0) + ? tr::lng_context_change_price(tr::now) + : tr::lng_context_make_paid(tr::now)), + [=] { action({ .type = ActionType::ChangePrice }, details); }, + &icons.menuPrice); + } using namespace HistoryView::Reactions; const auto effect = std::make_shared>(); diff --git a/Telegram/SourceFiles/menu/menu_send.h b/Telegram/SourceFiles/menu/menu_send.h index 2c6dbb9b7..5c3a99bae 100644 --- a/Telegram/SourceFiles/menu/menu_send.h +++ b/Telegram/SourceFiles/menu/menu_send.h @@ -53,6 +53,7 @@ struct Details { Type type = Type::Disabled; SpoilerState spoiler = SpoilerState::None; CaptionState caption = CaptionState::None; + std::optional price; bool effectAllowed = false; }; @@ -69,6 +70,7 @@ enum class ActionType : uchar { SpoilerOff, CaptionUp, CaptionDown, + ChangePrice, }; struct Action { using Type = ActionType; diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp index 112cdd576..aad25cdf5 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp @@ -27,7 +27,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" namespace Payments { -namespace { bool IsCreditsInvoice(not_null item) { if (const auto payment = item->Get()) { @@ -38,8 +37,6 @@ bool IsCreditsInvoice(not_null item) { return invoice && (invoice->currency == Ui::kCreditsCurrency); } -} // namespace - Fn ProcessNonPanelPaymentFormFactory( not_null controller, Fn maybeReturnToBot) { diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.h b/Telegram/SourceFiles/payments/payments_non_panel_process.h index fb647a72a..e8ab9375c 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.h +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.h @@ -18,6 +18,8 @@ namespace Payments { enum class CheckoutResult; struct NonPanelPaymentForm; +[[nodiscard]] bool IsCreditsInvoice(not_null item); + Fn ProcessNonPanelPaymentFormFactory( not_null controller, Fn maybeReturnToBot = nullptr); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp index 01b94a9d1..b142a61c1 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp @@ -32,9 +32,11 @@ constexpr auto kMinPreviewWidth = 20; AbstractSingleMediaPreview::AbstractSingleMediaPreview( QWidget *parent, const style::ComposeControls &st, - AttachControls::Type type) + AttachControls::Type type, + Fn canToggleSpoiler) : AbstractSinglePreview(parent) , _st(st) +, _canToggleSpoiler(std::move(canToggleSpoiler)) , _minThumbH(st::sendBoxAlbumGroupSize.height() + st::sendBoxAlbumGroupSkipTop * 2) , _controls(base::make_unique_q(this, type)) { @@ -266,7 +268,9 @@ void AbstractSingleMediaPreview::applyCursor(style::cursor cursor) { } void AbstractSingleMediaPreview::showContextMenu(QPoint position) { - if (!_sendWay.sendImagesAsPhotos() || !supportsSpoilers()) { + if (!_canToggleSpoiler() + || !_sendWay.sendImagesAsPhotos() + || !supportsSpoilers()) { return; } _menu = base::make_unique_q( diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h index 0f5f8e784..fd72facf1 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h @@ -26,7 +26,8 @@ public: AbstractSingleMediaPreview( QWidget *parent, const style::ComposeControls &st, - AttachControls::Type type); + AttachControls::Type type, + Fn canToggleSpoiler); ~AbstractSingleMediaPreview(); void setSendWay(SendFilesWay way); @@ -71,6 +72,7 @@ private: const style::ComposeControls &_st; SendFilesWay _sendWay; + Fn _canToggleSpoiler; bool _animated = false; QPixmap _preview; QPixmap _previewBlurred; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp index 9b842aae5..76ff1ab2b 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp @@ -31,10 +31,12 @@ AlbumPreview::AlbumPreview( QWidget *parent, const style::ComposeControls &st, gsl::span items, - SendFilesWay way) + SendFilesWay way, + Fn canToggleSpoiler) : RpWidget(parent) , _st(st) , _sendWay(way) +, _canToggleSpoiler(std::move(canToggleSpoiler)) , _dragTimer([=] { switchToDrag(); }) { setMouseTracking(true); prepareThumbs(items); @@ -573,7 +575,7 @@ void AlbumPreview::mouseReleaseEvent(QMouseEvent *e) { void AlbumPreview::showContextMenu( not_null thumb, QPoint position) { - if (!_sendWay.sendImagesAsPhotos()) { + if (!_canToggleSpoiler() || !_sendWay.sendImagesAsPhotos()) { return; } _menu = base::make_unique_q( diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h index 11eda122f..f51eb26e9 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h @@ -28,7 +28,8 @@ public: QWidget *parent, const style::ComposeControls &st, gsl::span items, - SendFilesWay way); + SendFilesWay way, + Fn canToggleSpoiler); ~AlbumPreview(); void setSendWay(SendFilesWay way); @@ -92,6 +93,7 @@ private: const style::ComposeControls &_st; SendFilesWay _sendWay; + Fn _canToggleSpoiler; style::cursor _cursor = style::cur_default; std::vector _order; std::vector _itemsShownDimensions; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp index b686ee6fe..d180059dd 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp @@ -36,7 +36,7 @@ ItemSingleMediaPreview::ItemSingleMediaPreview( Fn gifPaused, not_null item, AttachControls::Type type) -: AbstractSingleMediaPreview(parent, st, type) +: AbstractSingleMediaPreview(parent, st, type, [] { return true; }) , _gifPaused(std::move(gifPaused)) , _fullId(item->fullId()) { const auto media = item->media(); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp index 1a70c69b6..e3f90912f 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp @@ -195,6 +195,10 @@ bool PreparedList::canMoveCaption(bool sendingAlbum, bool compress) const { || (file.type == PreparedFile::Type::Photo && compress); } +bool PreparedList::canChangePrice(bool sendingAlbum, bool compress) const { + return canMoveCaption(sendingAlbum, compress); +} + bool PreparedList::hasGroupOption(bool slowmode) const { if (slowmode || files.size() < 2) { return false; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h index 30210e3d6..49fd71fa1 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h @@ -115,6 +115,9 @@ struct PreparedList { [[nodiscard]] bool canMoveCaption( bool sendingAlbum, bool compress) const; + [[nodiscard]] bool canChangePrice( + bool sendingAlbum, + bool compress) const; [[nodiscard]] bool canBeSentInSlowmode() const; [[nodiscard]] bool canBeSentInSlowmodeWith( const PreparedList &other) const; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp index ed250540c..cb80676cd 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp @@ -19,6 +19,7 @@ SingleMediaPreview *SingleMediaPreview::Create( const style::ComposeControls &st, Fn gifPaused, const PreparedFile &file, + Fn canToggleSpoiler, AttachControls::Type type) { auto preview = QImage(); auto animated = false; @@ -51,7 +52,8 @@ SingleMediaPreview *SingleMediaPreview::Create( Core::IsMimeSticker(file.information->filemime), file.spoiler, animationPreview ? file.path : QString(), - type); + type, + std::move(canToggleSpoiler)); } SingleMediaPreview::SingleMediaPreview( @@ -63,8 +65,9 @@ SingleMediaPreview::SingleMediaPreview( bool sticker, bool spoiler, const QString &animatedPreviewPath, - AttachControls::Type type) -: AbstractSingleMediaPreview(parent, st, type) + AttachControls::Type type, + Fn canToggleSpoiler) +: AbstractSingleMediaPreview(parent, st, type, std::move(canToggleSpoiler)) , _gifPaused(std::move(gifPaused)) , _sticker(sticker) { Expects(!preview.isNull()); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h index ac5d4f84f..d57ccd366 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h @@ -25,6 +25,7 @@ public: const style::ComposeControls &st, Fn gifPaused, const PreparedFile &file, + Fn canToggleSpoiler, AttachControls::Type type = AttachControls::Type::Full); SingleMediaPreview( @@ -36,7 +37,8 @@ public: bool sticker, bool spoiler, const QString &animatedPreviewPath, - AttachControls::Type type); + AttachControls::Type type, + Fn canToggleSpoiler); protected: bool supportsSpoilers() const override;