Edit price on send, send single paid media.

This commit is contained in:
John Preston 2024-06-18 18:55:07 +04:00
parent 3ece9b1566
commit a9bd7803e6
30 changed files with 382 additions and 37 deletions

View file

@ -3311,6 +3311,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_spoiler_effect" = "Hide with Spoiler"; "lng_context_spoiler_effect" = "Hide with Spoiler";
"lng_context_disable_spoiler" = "Remove 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_title" = "Fact Check";
"lng_factcheck_placeholder" = "Add Facts or Context"; "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_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_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_show_original" = "Show Original";
"lng_translate_bar_to" = "Translate to {name}"; "lng_translate_bar_to" = "Translate to {name}";
"lng_translate_bar_to_other" = "Translate to {name}"; "lng_translate_bar_to_other" = "Translate to {name}";

View file

@ -20,6 +20,7 @@ namespace Api {
inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE);
struct SendOptions { struct SendOptions {
uint64 price = 0;
PeerData *sendAs = nullptr; PeerData *sendAs = nullptr;
TimeId scheduled = 0; TimeId scheduled = 0;
BusinessShortcutId shortcutId = 0; BusinessShortcutId shortcutId = 0;

View file

@ -55,7 +55,9 @@ void ViewsManager::removeIncremented(not_null<PeerData*> peer) {
_incremented.remove(peer); _incremented.remove(peer);
} }
void ViewsManager::pollExtendedMedia(not_null<HistoryItem*> item) { void ViewsManager::pollExtendedMedia(
not_null<HistoryItem*> item,
bool force) {
if (!item->isRegular()) { if (!item->isRegular()) {
return; return;
} }
@ -63,14 +65,20 @@ void ViewsManager::pollExtendedMedia(not_null<HistoryItem*> item) {
const auto peer = item->history()->peer; const auto peer = item->history()->peer;
auto &request = _pollRequests[peer]; auto &request = _pollRequests[peer];
if (request.ids.contains(id) || request.sent.contains(id)) { if (request.ids.contains(id) || request.sent.contains(id)) {
return; if (!force || request.forced) {
return;
}
} }
request.ids.emplace(id); request.ids.emplace(id);
if (!request.id && !request.when) { if (force) {
request.when = crl::now() + kPollExtendedMediaPeriod; request.forced = true;
} }
if (!_pollTimer.isActive()) { const auto delay = force ? 1 : kPollExtendedMediaPeriod;
_pollTimer.callOnce(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()) { if (i->second.ids.empty()) {
i = _pollRequests.erase(i); i = _pollRequests.erase(i);
} else { } else {
i->second.when = now + kPollExtendedMediaPeriod; const auto delay = i->second.forced
if (!_pollTimer.isActive()) { ? 1
_pollTimer.callOnce(kPollExtendedMediaPeriod); : kPollExtendedMediaPeriod;
i->second.when = now + delay;
if (!_pollTimer.isActive() || i->second.forced) {
_pollTimer.callOnce(delay);
} }
++i; ++i;
} }

View file

@ -26,7 +26,7 @@ public:
void scheduleIncrement(not_null<HistoryItem*> item); void scheduleIncrement(not_null<HistoryItem*> item);
void removeIncremented(not_null<PeerData*> peer); void removeIncremented(not_null<PeerData*> peer);
void pollExtendedMedia(not_null<HistoryItem*> item); void pollExtendedMedia(not_null<HistoryItem*> item, bool force = false);
private: private:
struct PollExtendedMediaRequest { struct PollExtendedMediaRequest {
@ -34,6 +34,7 @@ private:
mtpRequestId id = 0; mtpRequestId id = 0;
base::flat_set<MsgId> ids; base::flat_set<MsgId> ids;
base::flat_set<MsgId> sent; base::flat_set<MsgId> sent;
bool forced = false;
}; };
void viewsIncrement(); void viewsIncrement();

View file

@ -4188,7 +4188,11 @@ void ApiWrap::sendMediaWithRandomId(
MTP_flags(flags), MTP_flags(flags),
peer->input, peer->input,
Data::Histories::ReplyToPlaceholder(), Data::Histories::ReplyToPlaceholder(),
media, (options.price
? MTPInputMedia(MTP_inputMediaPaidMedia(
MTP_long(options.price),
MTP_vector<MTPInputMedia>(1, media)))
: media),
MTP_string(caption.text), MTP_string(caption.text),
MTP_long(randomId), MTP_long(randomId),
MTPReplyMarkup(), MTPReplyMarkup(),

View file

@ -463,6 +463,7 @@ void EditCaptionBox::rebuildPreview() {
st::defaultComposeControls, st::defaultComposeControls,
gifPaused, gifPaused,
file, file,
[] { return true; },
Ui::AttachControls::Type::EditOnly); Ui::AttachControls::Type::EditOnly);
_isPhoto = (media && media->isPhoto()); _isPhoto = (media && media->isPhoto());
const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType); const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType);

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "storage/localstorage.h" #include "storage/localstorage.h"
#include "storage/storage_media_prepare.h" #include "storage/storage_media_prepare.h"
#include "iv/iv_instance.h"
#include "mainwidget.h" #include "mainwidget.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "main/main_session_settings.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_file_preview.h"
#include "ui/chat/attach/attach_single_media_preview.h" #include "ui/chat/attach/attach_single_media_preview.h"
#include "ui/grouped_layout.h" #include "ui/grouped_layout.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h" #include "ui/toast/toast.h"
#include "ui/controls/emoji_button.h" #include "ui/controls/emoji_button.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/vertical_list.h"
#include "lottie/lottie_single_player.h" #include "lottie/lottie_single_player.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_user.h" #include "data/data_user.h"
@ -58,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace { namespace {
constexpr auto kMaxMessageLength = 4096; constexpr auto kMaxMessageLength = 4096;
constexpr auto kMaxPrice = 1000ULL;
using Ui::SendFilesWay; using Ui::SendFilesWay;
@ -103,6 +107,74 @@ rpl::producer<QString> FieldPlaceholder(
: tr::lng_photos_comment(); : tr::lng_photos_comment();
} }
void EditPriceBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
uint64 price,
Fn<void(uint64)> 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<Ui::InputField>(
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<Ui::FlatLabel>(
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 } // namespace
SendFilesLimits DefaultLimitsForPeer(not_null<PeerData*> peer) { SendFilesLimits DefaultLimitsForPeer(not_null<PeerData*> peer) {
@ -153,7 +225,8 @@ SendFilesBox::Block::Block(
int from, int from,
int till, int till,
Fn<bool()> gifPaused, Fn<bool()> gifPaused,
SendFilesWay way) SendFilesWay way,
Fn<bool()> canToggleSpoiler)
: _items(items) : _items(items)
, _from(from) , _from(from)
, _till(till) { , _till(till) {
@ -170,14 +243,16 @@ SendFilesBox::Block::Block(
parent.get(), parent.get(),
st, st,
my, my,
way); way,
std::move(canToggleSpoiler));
_preview.reset(preview); _preview.reset(preview);
} else { } else {
const auto media = Ui::SingleMediaPreview::Create( const auto media = Ui::SingleMediaPreview::Create(
parent, parent,
st, st,
gifPaused, gifPaused,
first); first,
std::move(canToggleSpoiler));
if (media) { if (media) {
_isSingleMedia = true; _isSingleMedia = true;
_preview.reset(media); _preview.reset(media);
@ -385,6 +460,9 @@ Fn<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(
: _invertCaption : _invertCaption
? SendMenu::CaptionState::Above ? SendMenu::CaptionState::Above
: SendMenu::CaptionState::Below; : SendMenu::CaptionState::Below;
result.price = canChangePrice()
? _price.current()
: std::optional<uint64>();
return result; return result;
}); });
} }
@ -398,6 +476,7 @@ auto SendFilesBox::prepareSendMenuCallback()
case Type::CaptionUp: _invertCaption = true; break; case Type::CaptionUp: _invertCaption = true; break;
case Type::SpoilerOn: toggleSpoilers(true); break; case Type::SpoilerOn: toggleSpoilers(true); break;
case Type::SpoilerOff: toggleSpoilers(false); break; case Type::SpoilerOff: toggleSpoilers(false); break;
case Type::ChangePrice: changePrice(); break;
default: default:
SendMenu::DefaultCallback( SendMenu::DefaultCallback(
_show, _show,
@ -588,14 +667,22 @@ void SendFilesBox::refreshButtons() {
addMenuButton(); addMenuButton();
} }
bool SendFilesBox::hasSendMenu(const SendMenu::Details &details) const { bool SendFilesBox::hasSendMenu(const MenuDetails &details) const {
return (details.type != SendMenu::Type::Disabled) return (details.type != SendMenu::Type::Disabled)
|| (details.spoiler != SendMenu::SpoilerState::None) || (details.spoiler != SendMenu::SpoilerState::None)
|| (details.caption != SendMenu::CaptionState::None); || (details.caption != SendMenu::CaptionState::None);
} }
bool SendFilesBox::hasSpoilerMenu() const { 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() { 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<Ui::RpWidget>(_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<Ui::FlatLabel>(
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() { void SendFilesBox::addMenuButton() {
const auto details = _sendMenuDetails(); const auto details = _sendMenuDetails();
if (!hasSendMenu(details)) { if (!hasSendMenu(details)) {
@ -766,7 +918,8 @@ void SendFilesBox::pushBlock(int from, int till) {
from, from,
till, till,
gifPaused, gifPaused,
_sendWay.current()); _sendWay.current(),
[=] { return !hasPrice(); });
auto &block = _blocks.back(); auto &block = _blocks.back();
const auto widget = _inner->add( const auto widget = _inner->add(
block.takeWidget(), block.takeWidget(),
@ -893,6 +1046,7 @@ void SendFilesBox::pushBlock(int from, int till) {
void SendFilesBox::refreshControls(bool initial) { void SendFilesBox::refreshControls(bool initial) {
refreshButtons(); refreshButtons();
refreshPriceTag();
refreshTitleText(); refreshTitleText();
updateSendWayControls(); updateSendWayControls();
updateCaptionPlaceholder(); updateCaptionPlaceholder();
@ -1447,6 +1601,7 @@ void SendFilesBox::send(
auto child = _sendMenuDetails(); auto child = _sendMenuDetails();
child.spoiler = SendMenu::SpoilerState::None; child.spoiler = SendMenu::SpoilerState::None;
child.caption = SendMenu::CaptionState::None; child.caption = SendMenu::CaptionState::None;
child.price = std::nullopt;
return SendMenu::DefaultCallback(_show, sendCallback())( return SendMenu::DefaultCallback(_show, sendCallback())(
{ .type = SendMenu::ActionType::Schedule }, { .type = SendMenu::ActionType::Schedule },
child); child);
@ -1475,6 +1630,7 @@ void SendFilesBox::send(
? _caption->getTextWithAppliedMarkdown() ? _caption->getTextWithAppliedMarkdown()
: TextWithTags(); : TextWithTags();
options.invertCaption = _invertCaption; options.invertCaption = _invertCaption;
options.price = hasPrice() ? _price.current() : 0;
if (!validateLength(caption.text)) { if (!validateLength(caption.text)) {
return; return;
} }

View file

@ -149,7 +149,8 @@ private:
int from, int from,
int till, int till,
Fn<bool()> gifPaused, Fn<bool()> gifPaused,
Ui::SendFilesWay way); Ui::SendFilesWay way,
Fn<bool()> canToggleSpoiler);
Block(Block &&other) = default; Block(Block &&other) = default;
Block &operator=(Block &&other) = default; Block &operator=(Block &&other) = default;
@ -190,6 +191,11 @@ private:
void addMenuButton(); void addMenuButton();
void applyBlockChanges(); void applyBlockChanges();
void toggleSpoilers(bool enabled); void toggleSpoilers(bool enabled);
void changePrice();
[[nodiscard]] bool canChangePrice() const;
[[nodiscard]] bool hasPrice() const;
void refreshPriceTag();
bool validateLength(const QString &text) const; bool validateLength(const QString &text) const;
void refreshButtons(); void refreshButtons();
@ -251,6 +257,8 @@ private:
SendFilesCheck _check; SendFilesCheck _check;
SendFilesConfirmed _confirmedCallback; SendFilesConfirmed _confirmedCallback;
Fn<void()> _cancelledCallback; Fn<void()> _cancelledCallback;
rpl::variable<uint64> _price = 0;
std::unique_ptr<Ui::RpWidget> _priceTag;
bool _confirmed = false; bool _confirmed = false;
bool _invertCaption = false; bool _invertCaption = false;

View file

@ -71,6 +71,7 @@ ComposeIcons {
menuSpoilerOff: icon; menuSpoilerOff: icon;
menuBelow: icon; menuBelow: icon;
menuAbove: icon; menuAbove: icon;
menuPrice: icon;
stripBubble: icon; stripBubble: icon;
stripExpandPanel: icon; stripExpandPanel: icon;
@ -610,6 +611,7 @@ defaultComposeIcons: ComposeIcons {
menuSpoilerOff: menuIconSpoilerOff; menuSpoilerOff: menuIconSpoilerOff;
menuBelow: menuIconBelow; menuBelow: menuIconBelow;
menuAbove: menuIconAbove; menuAbove: menuIconAbove;
menuPrice: menuIconEarn;
stripBubble: icon{ stripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg }, { "chat/reactions_bubble_shadow", windowShadowFg },
@ -1406,3 +1408,15 @@ editTagField: InputField(defaultInputField) {
editTagLimit: FlatLabel(defaultFlatLabel) { editTagLimit: FlatLabel(defaultFlatLabel) {
textFg: windowSubTextFg; 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);

View file

@ -170,6 +170,12 @@ void UpdateCloudFile(
if (data.progressivePartSize && !file.location.valid()) { if (data.progressivePartSize && !file.location.valid()) {
file.progressivePartSize = data.progressivePartSize; file.progressivePartSize = data.progressivePartSize;
} }
if (data.location.width()
&& data.location.height()
&& !file.location.valid()
&& !file.location.width()) {
file.location = data.location;
}
return; return;
} }

View file

@ -323,7 +323,7 @@ bool UpdateExtendedMedia(
auto changed = false; auto changed = false;
const auto count = int(media.size()); const auto count = int(media.size());
for (auto i = 0; i != count; ++i) { for (auto i = 0; i != count; ++i) {
if (i < invoice.extendedMedia.size()) { if (i <= invoice.extendedMedia.size()) {
invoice.extendedMedia.emplace_back(); invoice.extendedMedia.emplace_back();
changed = true; changed = true;
} }
@ -388,6 +388,7 @@ Invoice ComputeInvoiceData(
auto result = Invoice{ auto result = Invoice{
.amount = data.vstars_amount().v, .amount = data.vstars_amount().v,
.currency = Ui::kCreditsCurrency, .currency = Ui::kCreditsCurrency,
.isPaidMedia = true,
}; };
UpdateExtendedMedia(result, item, data.vextended_media().v); UpdateExtendedMedia(result, item, data.vextended_media().v);
return result; return result;
@ -1908,6 +1909,7 @@ MediaInvoice::MediaInvoice(
.title = data.title, .title = data.title,
.description = data.description, .description = data.description,
.photo = data.photo, .photo = data.photo,
.isPaidMedia = data.isPaidMedia,
.isTest = data.isTest, .isTest = data.isTest,
} { } {
_invoice.extendedMedia.reserve(data.extendedMedia.size()); _invoice.extendedMedia.reserve(data.extendedMedia.size());

View file

@ -94,6 +94,7 @@ struct Invoice {
TextWithEntities description; TextWithEntities description;
std::vector<std::unique_ptr<Media>> extendedMedia; std::vector<std::unique_ptr<Media>> extendedMedia;
PhotoData *photo = nullptr; PhotoData *photo = nullptr;
bool isPaidMedia = false;
bool isTest = false; bool isTest = false;
}; };
[[nodiscard]] bool HasExtendedMedia(const Invoice &invoice); [[nodiscard]] bool HasExtendedMedia(const Invoice &invoice);

View file

@ -7,9 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "history/view/media/history_view_media_common.h" #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/text/format_values.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "core/click_handler_types.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_wall_paper.h" #include "data/data_wall_paper.h"
#include "data/data_media_types.h" #include "data/data_media_types.h"
#include "history/view/history_view_element.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_document.h"
#include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_theme_document.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 "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" #include "styles/style_chat.h"
namespace HistoryView { namespace HistoryView {
@ -180,4 +191,34 @@ QSize CountPhotoMediaSize(
media.scaled(media.width(), newWidth, Qt::KeepAspectRatio)); media.scaled(media.width(), newWidth, Qt::KeepAspectRatio));
} }
ClickHandlerPtr MakePaidMediaLink(not_null<HistoryItem*> item) {
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
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<void(Payments::CheckoutResult)>()),
((controller && Payments::IsCreditsInvoice(item))
? Payments::ProcessNonPanelPaymentFormFactory(
controller,
done)
: nullptr));
});
}
} // namespace HistoryView } // namespace HistoryView

View file

@ -75,4 +75,6 @@ void PaintInterpolatedIcon(
int newWidth, int newWidth,
int maxWidth); int maxWidth);
[[nodiscard]] ClickHandlerPtr MakePaidMediaLink(not_null<HistoryItem*> item);
} // namespace HistoryView } // namespace HistoryView

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_cursor_state.h" #include "history/view/history_view_cursor_state.h"
#include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_common.h"
#include "history/view/media/history_view_media_spoiler.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_instance.h"
#include "media/streaming/media_streaming_player.h" #include "media/streaming/media_streaming_player.h"
#include "media/streaming/media_streaming_document.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 "data/data_web_page.h"
#include "core/application.h" #include "core/application.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace HistoryView { namespace HistoryView {
namespace { namespace {
@ -140,7 +142,8 @@ void Photo::dataMediaCreated() const {
if (_data->inlineThumbnailBytes().isEmpty() if (_data->inlineThumbnailBytes().isEmpty()
&& !_dataMedia->image(PhotoSize::Large) && !_dataMedia->image(PhotoSize::Large)
&& !_dataMedia->image(PhotoSize::Thumbnail)) { && !_dataMedia->image(PhotoSize::Thumbnail)
&& !_data->extendedMediaPreview()) {
_dataMedia->wanted(PhotoSize::Small, _realParent->fullId()); _dataMedia->wanted(PhotoSize::Small, _realParent->fullId());
} }
history()->owner().registerHeavyViewPart(_parent); history()->owner().registerHeavyViewPart(_parent);
@ -277,8 +280,9 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
_dataMedia->automaticLoad(_realParent->fullId(), _parent->data()); _dataMedia->automaticLoad(_realParent->fullId(), _parent->data());
const auto st = context.st; const auto st = context.st;
const auto sti = context.imageStyle(); const auto sti = context.imageStyle();
auto loaded = _dataMedia->loaded(); const auto preview = _data->extendedMediaPreview();
auto displayLoading = _data->displayLoading(); auto loaded = preview || _dataMedia->loaded();
auto displayLoading = !preview && _data->displayLoading();
auto inWebPage = (_parent->media() != this); auto inWebPage = (_parent->media() != this);
auto paintx = 0, painty = 0, paintw = width(), painth = height(); 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))); QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
_animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg); _animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg);
} }
} else if (preview) {
paintPriceTag(p, rthumb);
} }
if (showEnlarge) { if (showEnlarge) {
auto hq = PainterHighQualityEnabler(p); 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 { void Photo::validateUserpicImageCache(QSize size, bool forum) const {
const auto forumValue = forum ? 1 : 0; const auto forumValue = forum ? 1 : 0;
const auto large = _dataMedia->image(PhotoSize::Large); 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 { TextState Photo::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent); auto result = TextState(_parent);
@ -617,7 +668,9 @@ TextState Photo::textState(QPoint point, StateRequest request) const {
if (QRect(paintx, painty, paintw, painth).contains(point)) { if (QRect(paintx, painty, paintw, painth).contains(point)) {
ensureDataMediaCreated(); ensureDataMediaCreated();
result.link = (_spoiler && !_spoiler->revealed) result.link = _data->extendedMediaPreview()
? ensureExtendedMediaLink()
: (_spoiler && !_spoiler->revealed)
? _spoiler->link ? _spoiler->link
: _data->uploading() : _data->uploading()
? _cancell ? _cancell

View file

@ -147,10 +147,13 @@ private:
[[nodiscard]] QSize photoSize() const; [[nodiscard]] QSize photoSize() const;
[[nodiscard]] QRect enlargeRect() const; [[nodiscard]] QRect enlargeRect() const;
void paintPriceTag(Painter &p, QRect rthumb) const;
[[nodiscard]] ClickHandlerPtr ensureExtendedMediaLink() const;
void togglePollingStory(bool enabled) const; void togglePollingStory(bool enabled) const;
const not_null<PhotoData*> _data; const not_null<PhotoData*> _data;
const FullStoryId _storyId; const FullStoryId _storyId;
mutable ClickHandlerPtr _extendedMediaLink;
mutable std::shared_ptr<Data::PhotoMedia> _dataMedia; mutable std::shared_ptr<Data::PhotoMedia> _dataMedia;
mutable std::unique_ptr<Streamed> _streamed; mutable std::unique_ptr<Streamed> _streamed;
const std::unique_ptr<MediaSpoiler> _spoiler; const std::unique_ptr<MediaSpoiler> _spoiler;

View file

@ -638,6 +638,7 @@ storiesEmojiPan: EmojiPan(defaultEmojiPan) {
menuSpoilerOff: icon {{ "menu/spoiler_off", storiesComposeWhiteText }}; menuSpoilerOff: icon {{ "menu/spoiler_off", storiesComposeWhiteText }};
menuBelow: icon {{ "menu/link_below", storiesComposeWhiteText }}; menuBelow: icon {{ "menu/link_below", storiesComposeWhiteText }};
menuAbove: icon {{ "menu/link_above", storiesComposeWhiteText }}; menuAbove: icon {{ "menu/link_above", storiesComposeWhiteText }};
menuPrice: icon {{ "menu/earn", storiesComposeWhiteText }};
stripBubble: icon{ stripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg }, { "chat/reactions_bubble_shadow", windowShadowFg },

View file

@ -678,6 +678,14 @@ FillMenuResult FillSendMenu(
}, details); }, }, details); },
above ? &icons.menuBelow : &icons.menuAbove); 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; using namespace HistoryView::Reactions;
const auto effect = std::make_shared<QPointer<EffectPreview>>(); const auto effect = std::make_shared<QPointer<EffectPreview>>();

View file

@ -53,6 +53,7 @@ struct Details {
Type type = Type::Disabled; Type type = Type::Disabled;
SpoilerState spoiler = SpoilerState::None; SpoilerState spoiler = SpoilerState::None;
CaptionState caption = CaptionState::None; CaptionState caption = CaptionState::None;
std::optional<uint64> price;
bool effectAllowed = false; bool effectAllowed = false;
}; };
@ -69,6 +70,7 @@ enum class ActionType : uchar {
SpoilerOff, SpoilerOff,
CaptionUp, CaptionUp,
CaptionDown, CaptionDown,
ChangePrice,
}; };
struct Action { struct Action {
using Type = ActionType; using Type = ActionType;

View file

@ -27,7 +27,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
namespace Payments { namespace Payments {
namespace {
bool IsCreditsInvoice(not_null<HistoryItem*> item) { bool IsCreditsInvoice(not_null<HistoryItem*> item) {
if (const auto payment = item->Get<HistoryServicePayment>()) { if (const auto payment = item->Get<HistoryServicePayment>()) {
@ -38,8 +37,6 @@ bool IsCreditsInvoice(not_null<HistoryItem*> item) {
return invoice && (invoice->currency == Ui::kCreditsCurrency); return invoice && (invoice->currency == Ui::kCreditsCurrency);
} }
} // namespace
Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory( Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
Fn<void(CheckoutResult)> maybeReturnToBot) { Fn<void(CheckoutResult)> maybeReturnToBot) {

View file

@ -18,6 +18,8 @@ namespace Payments {
enum class CheckoutResult; enum class CheckoutResult;
struct NonPanelPaymentForm; struct NonPanelPaymentForm;
[[nodiscard]] bool IsCreditsInvoice(not_null<HistoryItem*> item);
Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory( Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
Fn<void(Payments::CheckoutResult)> maybeReturnToBot = nullptr); Fn<void(Payments::CheckoutResult)> maybeReturnToBot = nullptr);

View file

@ -32,9 +32,11 @@ constexpr auto kMinPreviewWidth = 20;
AbstractSingleMediaPreview::AbstractSingleMediaPreview( AbstractSingleMediaPreview::AbstractSingleMediaPreview(
QWidget *parent, QWidget *parent,
const style::ComposeControls &st, const style::ComposeControls &st,
AttachControls::Type type) AttachControls::Type type,
Fn<bool()> canToggleSpoiler)
: AbstractSinglePreview(parent) : AbstractSinglePreview(parent)
, _st(st) , _st(st)
, _canToggleSpoiler(std::move(canToggleSpoiler))
, _minThumbH(st::sendBoxAlbumGroupSize.height() , _minThumbH(st::sendBoxAlbumGroupSize.height()
+ st::sendBoxAlbumGroupSkipTop * 2) + st::sendBoxAlbumGroupSkipTop * 2)
, _controls(base::make_unique_q<AttachControlsWidget>(this, type)) { , _controls(base::make_unique_q<AttachControlsWidget>(this, type)) {
@ -266,7 +268,9 @@ void AbstractSingleMediaPreview::applyCursor(style::cursor cursor) {
} }
void AbstractSingleMediaPreview::showContextMenu(QPoint position) { void AbstractSingleMediaPreview::showContextMenu(QPoint position) {
if (!_sendWay.sendImagesAsPhotos() || !supportsSpoilers()) { if (!_canToggleSpoiler()
|| !_sendWay.sendImagesAsPhotos()
|| !supportsSpoilers()) {
return; return;
} }
_menu = base::make_unique_q<Ui::PopupMenu>( _menu = base::make_unique_q<Ui::PopupMenu>(

View file

@ -26,7 +26,8 @@ public:
AbstractSingleMediaPreview( AbstractSingleMediaPreview(
QWidget *parent, QWidget *parent,
const style::ComposeControls &st, const style::ComposeControls &st,
AttachControls::Type type); AttachControls::Type type,
Fn<bool()> canToggleSpoiler);
~AbstractSingleMediaPreview(); ~AbstractSingleMediaPreview();
void setSendWay(SendFilesWay way); void setSendWay(SendFilesWay way);
@ -71,6 +72,7 @@ private:
const style::ComposeControls &_st; const style::ComposeControls &_st;
SendFilesWay _sendWay; SendFilesWay _sendWay;
Fn<bool()> _canToggleSpoiler;
bool _animated = false; bool _animated = false;
QPixmap _preview; QPixmap _preview;
QPixmap _previewBlurred; QPixmap _previewBlurred;

View file

@ -31,10 +31,12 @@ AlbumPreview::AlbumPreview(
QWidget *parent, QWidget *parent,
const style::ComposeControls &st, const style::ComposeControls &st,
gsl::span<Ui::PreparedFile> items, gsl::span<Ui::PreparedFile> items,
SendFilesWay way) SendFilesWay way,
Fn<bool()> canToggleSpoiler)
: RpWidget(parent) : RpWidget(parent)
, _st(st) , _st(st)
, _sendWay(way) , _sendWay(way)
, _canToggleSpoiler(std::move(canToggleSpoiler))
, _dragTimer([=] { switchToDrag(); }) { , _dragTimer([=] { switchToDrag(); }) {
setMouseTracking(true); setMouseTracking(true);
prepareThumbs(items); prepareThumbs(items);
@ -573,7 +575,7 @@ void AlbumPreview::mouseReleaseEvent(QMouseEvent *e) {
void AlbumPreview::showContextMenu( void AlbumPreview::showContextMenu(
not_null<AlbumThumbnail*> thumb, not_null<AlbumThumbnail*> thumb,
QPoint position) { QPoint position) {
if (!_sendWay.sendImagesAsPhotos()) { if (!_canToggleSpoiler() || !_sendWay.sendImagesAsPhotos()) {
return; return;
} }
_menu = base::make_unique_q<Ui::PopupMenu>( _menu = base::make_unique_q<Ui::PopupMenu>(

View file

@ -28,7 +28,8 @@ public:
QWidget *parent, QWidget *parent,
const style::ComposeControls &st, const style::ComposeControls &st,
gsl::span<Ui::PreparedFile> items, gsl::span<Ui::PreparedFile> items,
SendFilesWay way); SendFilesWay way,
Fn<bool()> canToggleSpoiler);
~AlbumPreview(); ~AlbumPreview();
void setSendWay(SendFilesWay way); void setSendWay(SendFilesWay way);
@ -92,6 +93,7 @@ private:
const style::ComposeControls &_st; const style::ComposeControls &_st;
SendFilesWay _sendWay; SendFilesWay _sendWay;
Fn<bool()> _canToggleSpoiler;
style::cursor _cursor = style::cur_default; style::cursor _cursor = style::cur_default;
std::vector<int> _order; std::vector<int> _order;
std::vector<QSize> _itemsShownDimensions; std::vector<QSize> _itemsShownDimensions;

View file

@ -36,7 +36,7 @@ ItemSingleMediaPreview::ItemSingleMediaPreview(
Fn<bool()> gifPaused, Fn<bool()> gifPaused,
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
AttachControls::Type type) AttachControls::Type type)
: AbstractSingleMediaPreview(parent, st, type) : AbstractSingleMediaPreview(parent, st, type, [] { return true; })
, _gifPaused(std::move(gifPaused)) , _gifPaused(std::move(gifPaused))
, _fullId(item->fullId()) { , _fullId(item->fullId()) {
const auto media = item->media(); const auto media = item->media();

View file

@ -195,6 +195,10 @@ bool PreparedList::canMoveCaption(bool sendingAlbum, bool compress) const {
|| (file.type == PreparedFile::Type::Photo && compress); || (file.type == PreparedFile::Type::Photo && compress);
} }
bool PreparedList::canChangePrice(bool sendingAlbum, bool compress) const {
return canMoveCaption(sendingAlbum, compress);
}
bool PreparedList::hasGroupOption(bool slowmode) const { bool PreparedList::hasGroupOption(bool slowmode) const {
if (slowmode || files.size() < 2) { if (slowmode || files.size() < 2) {
return false; return false;

View file

@ -115,6 +115,9 @@ struct PreparedList {
[[nodiscard]] bool canMoveCaption( [[nodiscard]] bool canMoveCaption(
bool sendingAlbum, bool sendingAlbum,
bool compress) const; bool compress) const;
[[nodiscard]] bool canChangePrice(
bool sendingAlbum,
bool compress) const;
[[nodiscard]] bool canBeSentInSlowmode() const; [[nodiscard]] bool canBeSentInSlowmode() const;
[[nodiscard]] bool canBeSentInSlowmodeWith( [[nodiscard]] bool canBeSentInSlowmodeWith(
const PreparedList &other) const; const PreparedList &other) const;

View file

@ -19,6 +19,7 @@ SingleMediaPreview *SingleMediaPreview::Create(
const style::ComposeControls &st, const style::ComposeControls &st,
Fn<bool()> gifPaused, Fn<bool()> gifPaused,
const PreparedFile &file, const PreparedFile &file,
Fn<bool()> canToggleSpoiler,
AttachControls::Type type) { AttachControls::Type type) {
auto preview = QImage(); auto preview = QImage();
auto animated = false; auto animated = false;
@ -51,7 +52,8 @@ SingleMediaPreview *SingleMediaPreview::Create(
Core::IsMimeSticker(file.information->filemime), Core::IsMimeSticker(file.information->filemime),
file.spoiler, file.spoiler,
animationPreview ? file.path : QString(), animationPreview ? file.path : QString(),
type); type,
std::move(canToggleSpoiler));
} }
SingleMediaPreview::SingleMediaPreview( SingleMediaPreview::SingleMediaPreview(
@ -63,8 +65,9 @@ SingleMediaPreview::SingleMediaPreview(
bool sticker, bool sticker,
bool spoiler, bool spoiler,
const QString &animatedPreviewPath, const QString &animatedPreviewPath,
AttachControls::Type type) AttachControls::Type type,
: AbstractSingleMediaPreview(parent, st, type) Fn<bool()> canToggleSpoiler)
: AbstractSingleMediaPreview(parent, st, type, std::move(canToggleSpoiler))
, _gifPaused(std::move(gifPaused)) , _gifPaused(std::move(gifPaused))
, _sticker(sticker) { , _sticker(sticker) {
Expects(!preview.isNull()); Expects(!preview.isNull());

View file

@ -25,6 +25,7 @@ public:
const style::ComposeControls &st, const style::ComposeControls &st,
Fn<bool()> gifPaused, Fn<bool()> gifPaused,
const PreparedFile &file, const PreparedFile &file,
Fn<bool()> canToggleSpoiler,
AttachControls::Type type = AttachControls::Type::Full); AttachControls::Type type = AttachControls::Type::Full);
SingleMediaPreview( SingleMediaPreview(
@ -36,7 +37,8 @@ public:
bool sticker, bool sticker,
bool spoiler, bool spoiler,
const QString &animatedPreviewPath, const QString &animatedPreviewPath,
AttachControls::Type type); AttachControls::Type type,
Fn<bool()> canToggleSpoiler);
protected: protected:
bool supportsSpoilers() const override; bool supportsSpoilers() const override;