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_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}";

View file

@ -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;

View file

@ -55,7 +55,9 @@ void ViewsManager::removeIncremented(not_null<PeerData*> peer) {
_incremented.remove(peer);
}
void ViewsManager::pollExtendedMedia(not_null<HistoryItem*> item) {
void ViewsManager::pollExtendedMedia(
not_null<HistoryItem*> item,
bool force) {
if (!item->isRegular()) {
return;
}
@ -63,14 +65,20 @@ void ViewsManager::pollExtendedMedia(not_null<HistoryItem*> 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;
}

View file

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

View file

@ -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<MTPInputMedia>(1, media)))
: media),
MTP_string(caption.text),
MTP_long(randomId),
MTPReplyMarkup(),

View file

@ -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);

View file

@ -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<QString> FieldPlaceholder(
: 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
SendFilesLimits DefaultLimitsForPeer(not_null<PeerData*> peer) {
@ -153,7 +225,8 @@ SendFilesBox::Block::Block(
int from,
int till,
Fn<bool()> gifPaused,
SendFilesWay way)
SendFilesWay way,
Fn<bool()> 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<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(
: _invertCaption
? SendMenu::CaptionState::Above
: SendMenu::CaptionState::Below;
result.price = canChangePrice()
? _price.current()
: std::optional<uint64>();
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<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() {
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;
}

View file

@ -149,7 +149,8 @@ private:
int from,
int till,
Fn<bool()> gifPaused,
Ui::SendFilesWay way);
Ui::SendFilesWay way,
Fn<bool()> 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<void()> _cancelledCallback;
rpl::variable<uint64> _price = 0;
std::unique_ptr<Ui::RpWidget> _priceTag;
bool _confirmed = false;
bool _invertCaption = false;

View file

@ -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);

View file

@ -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;
}

View file

@ -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());

View file

@ -94,6 +94,7 @@ struct Invoice {
TextWithEntities description;
std::vector<std::unique_ptr<Media>> extendedMedia;
PhotoData *photo = nullptr;
bool isPaidMedia = false;
bool isTest = false;
};
[[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 "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<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

View file

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

View file

@ -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<PhotoData*> _data;
const FullStoryId _storyId;
mutable ClickHandlerPtr _extendedMediaLink;
mutable std::shared_ptr<Data::PhotoMedia> _dataMedia;
mutable std::unique_ptr<Streamed> _streamed;
const std::unique_ptr<MediaSpoiler> _spoiler;

View file

@ -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 },

View file

@ -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<QPointer<EffectPreview>>();

View file

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

View file

@ -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<HistoryItem*> item) {
if (const auto payment = item->Get<HistoryServicePayment>()) {
@ -38,8 +37,6 @@ bool IsCreditsInvoice(not_null<HistoryItem*> item) {
return invoice && (invoice->currency == Ui::kCreditsCurrency);
}
} // namespace
Fn<void(NonPanelPaymentForm)> ProcessNonPanelPaymentFormFactory(
not_null<Window::SessionController*> controller,
Fn<void(CheckoutResult)> maybeReturnToBot) {

View file

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

View file

@ -32,9 +32,11 @@ constexpr auto kMinPreviewWidth = 20;
AbstractSingleMediaPreview::AbstractSingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
AttachControls::Type type)
AttachControls::Type type,
Fn<bool()> canToggleSpoiler)
: AbstractSinglePreview(parent)
, _st(st)
, _canToggleSpoiler(std::move(canToggleSpoiler))
, _minThumbH(st::sendBoxAlbumGroupSize.height()
+ st::sendBoxAlbumGroupSkipTop * 2)
, _controls(base::make_unique_q<AttachControlsWidget>(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<Ui::PopupMenu>(

View file

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

View file

@ -31,10 +31,12 @@ AlbumPreview::AlbumPreview(
QWidget *parent,
const style::ComposeControls &st,
gsl::span<Ui::PreparedFile> items,
SendFilesWay way)
SendFilesWay way,
Fn<bool()> 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<AlbumThumbnail*> thumb,
QPoint position) {
if (!_sendWay.sendImagesAsPhotos()) {
if (!_canToggleSpoiler() || !_sendWay.sendImagesAsPhotos()) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(

View file

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

View file

@ -36,7 +36,7 @@ ItemSingleMediaPreview::ItemSingleMediaPreview(
Fn<bool()> gifPaused,
not_null<HistoryItem*> 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();

View file

@ -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;

View file

@ -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;

View file

@ -19,6 +19,7 @@ SingleMediaPreview *SingleMediaPreview::Create(
const style::ComposeControls &st,
Fn<bool()> gifPaused,
const PreparedFile &file,
Fn<bool()> 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<bool()> canToggleSpoiler)
: AbstractSingleMediaPreview(parent, st, type, std::move(canToggleSpoiler))
, _gifPaused(std::move(gifPaused))
, _sticker(sticker) {
Expects(!preview.isNull());

View file

@ -25,6 +25,7 @@ public:
const style::ComposeControls &st,
Fn<bool()> gifPaused,
const PreparedFile &file,
Fn<bool()> 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<bool()> canToggleSpoiler);
protected:
bool supportsSpoilers() const override;