Support entities in star gift messages.

This commit is contained in:
John Preston 2024-09-26 22:05:58 +04:00
parent 8b11d2d5e7
commit 9ace04d2c9
11 changed files with 191 additions and 79 deletions

View file

@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_premium.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/send_credits_box.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/stickers_gift_box_pack.h"
#include "chat_helpers/stickers_lottie.h"
#include "core/ui_integration.h"
@ -65,7 +67,7 @@ namespace {
constexpr auto kPriceTabAll = 0;
constexpr auto kPriceTabLimited = -1;
constexpr auto kGiftMessageLimit = 256;
constexpr auto kGiftMessageLimit = 255;
constexpr auto kSentToastDuration = 3 * crl::time(1000);
using namespace HistoryView;
@ -83,7 +85,7 @@ struct GiftsDescriptor {
struct GiftDetails {
GiftDescriptor descriptor;
QString text;
TextWithEntities text;
bool anonymous = false;
};
@ -160,14 +162,16 @@ auto GenerateGiftMedia(
auto pushText = [&](
TextWithEntities text,
QMargins margins = {},
const base::flat_map<uint16, ClickHandlerPtr> &links = {}) {
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
const std::any &context = {}) {
if (text.empty()) {
return;
}
push(std::make_unique<MediaGenericTextPart>(
std::move(text),
margins,
links));
links,
context));
};
const auto sticker = [=] {
using Tag = ChatHelpers::StickerLottieSize;
@ -202,11 +206,18 @@ auto GenerateGiftMedia(
lt_count,
v::get<GiftTypeStars>(descriptor).convertStars,
Ui::Text::RichLangValue);
auto description = data.text.isEmpty()
auto description = data.text.empty()
? std::move(textFallback)
: TextWithEntities{ data.text };
: data.text;
pushText(Ui::Text::Bold(title), st::giftBoxPreviewTitlePadding);
pushText(std::move(description), st::giftBoxPreviewTextPadding);
pushText(
std::move(description),
st::giftBoxPreviewTextPadding,
{},
Core::MarkedTextContext{
.session = &parent->data()->history()->session(),
.customEmojiRepaint = [parent] { parent->repaint(); },
});
};
}
@ -732,10 +743,6 @@ void SendGiftBox(
});
const auto session = &window->session();
const auto context = Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [] {},
};
auto cost = rpl::single([&] {
return v::match(descriptor, [&](const GiftTypePremium &data) {
if (data.currency == Ui::kCreditsCurrency) {
@ -777,10 +784,40 @@ void SendGiftBox(
kGiftMessageLimit);
text->changes() | rpl::start_with_next([=] {
auto now = state->details.current();
now.text = text->getLastText();
auto textWithTags = text->getTextWithAppliedMarkdown();
now.text = TextWithEntities{
std::move(textWithTags.text),
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags)
};
state->details = std::move(now);
}, text->lifetime());
const auto allow = [=](not_null<DocumentData*> emoji) {
return true;
};
InitMessageFieldHandlers({
.session = &window->session(),
.show = window->uiShow(),
.field = text,
.customEmojiPaused = [=] {
using namespace Window;
return window->isGifPausedAtLeastFor(GifPauseReason::Layer);
},
.allowPremiumEmoji = allow,
.allowMarkdownTags = {
Ui::InputField::kTagBold,
Ui::InputField::kTagItalic,
Ui::InputField::kTagUnderline,
Ui::InputField::kTagStrikeOut,
Ui::InputField::kTagSpoiler,
}
});
Ui::Emoji::SuggestionsController::Init(
box->getDelegate()->outerContainer(),
text,
&window->session(),
{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
AddDivider(container);
AddSkip(container);
container->add(
@ -825,7 +862,7 @@ void SendGiftBox(
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
.giftId = gift.id,
.randomId = state->randomId,
.message = { details.text },
.message = details.text,
.user = peer->asUser(),
.anonymous = details.anonymous,
}, done, Payments::ProcessNonPanelPaymentFormFactory(window, done));

View file

@ -1267,13 +1267,16 @@ void SendFilesBox::setupCaption() {
: (_limits & SendFilesAllow::EmojiWithoutPremium);
};
const auto show = _show;
InitMessageFieldHandlers(
&show->session(),
show,
_caption.data(),
[=] { return show->paused(Window::GifPauseReason::Layer); },
allow,
&_st.files.caption);
InitMessageFieldHandlers({
.session = &show->session(),
.show = show,
.field = _caption.data(),
.customEmojiPaused = [=] {
return show->paused(Window::GifPauseReason::Layer);
},
.allowPremiumEmoji = allow,
.fieldStyle = &_st.files.caption,
});
setupCaptionAutocomplete();
Ui::Emoji::SuggestionsController::Init(
getDelegate()->outerContainer(),

View file

@ -240,13 +240,12 @@ void ShareBox::prepareCommentField() {
}, field->lifetime());
if (const auto show = uiShow(); show->valid()) {
InitMessageFieldHandlers(
_descriptor.session,
Main::MakeSessionShow(show, _descriptor.session),
field,
nullptr,
nullptr,
_descriptor.stLabel);
InitMessageFieldHandlers({
.session = _descriptor.session,
.show = Main::MakeSessionShow(show, _descriptor.session),
.field = field,
.fieldStyle = _descriptor.stLabel,
});
}
field->setSubmitSettings(Core::App().settings().sendSubmitWay());

View file

@ -423,18 +423,14 @@ Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
};
}
void InitMessageFieldHandlers(
not_null<Main::Session*> session,
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::InputField*> field,
Fn<bool()> customEmojiPaused,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji,
const style::InputField *fieldStyle) {
const auto paused = [customEmojiPaused] {
return customEmojiPaused && customEmojiPaused();
void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args) {
const auto paused = [passed = args.customEmojiPaused] {
return passed && passed();
};
const auto field = args.field;
const auto session = args.session;
field->setTagMimeProcessor(
FieldTagMimeProcessor(session, allowPremiumEmoji));
FieldTagMimeProcessor(session, args.allowPremiumEmoji));
field->setCustomTextContext([=](Fn<void()> repaint) {
return std::any(Core::MarkedTextContext{
.session = session,
@ -448,12 +444,14 @@ void InitMessageFieldHandlers(
field->setInstantReplaces(Ui::InstantReplaces::Default());
field->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue());
field->setMarkdownReplacesEnabled(true);
if (show) {
field->setMarkdownReplacesEnabled(rpl::single(Ui::MarkdownEnabledState{
Ui::MarkdownEnabled{ std::move(args.allowMarkdownTags) }
}));
if (const auto &show = args.show) {
field->setEditLinkCallback(
DefaultEditLinkCallback(show, field, fieldStyle));
DefaultEditLinkCallback(show, field, args.fieldStyle));
field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
InitSpellchecker(show, field, fieldStyle != nullptr);
InitSpellchecker(show, field, args.fieldStyle != nullptr);
}
const auto style = field->lifetime().make_state<Ui::ChatStyle>(
session->colorIndicesValue());
@ -553,12 +551,15 @@ void InitMessageFieldHandlers(
not_null<Ui::InputField*> field,
ChatHelpers::PauseReason pauseReasonLevel,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
InitMessageFieldHandlers(
&controller->session(),
controller->uiShow(),
field,
[=] { return controller->isGifPausedAtLeastFor(pauseReasonLevel); },
allowPremiumEmoji);
InitMessageFieldHandlers({
.session = &controller->session(),
.show = controller->uiShow(),
.field = field,
.customEmojiPaused = [=] {
return controller->isGifPausedAtLeastFor(pauseReasonLevel);
},
.allowPremiumEmoji = std::move(allowPremiumEmoji),
});
}
void InitMessageFieldGeometry(not_null<Ui::InputField*> field) {
@ -574,12 +575,15 @@ void InitMessageField(
std::shared_ptr<ChatHelpers::Show> show,
not_null<Ui::InputField*> field,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
InitMessageFieldHandlers(
&show->session(),
show,
field,
[=] { return show->paused(ChatHelpers::PauseReason::Any); },
std::move(allowPremiumEmoji));
InitMessageFieldHandlers({
.session = &show->session(),
.show = show,
.field = field,
.customEmojiPaused = [=] {
return show->paused(ChatHelpers::PauseReason::Any);
},
.allowPremiumEmoji = std::move(allowPremiumEmoji),
});
InitMessageFieldGeometry(field);
}

View file

@ -54,13 +54,18 @@ Fn<bool(
const style::InputField *fieldStyle = nullptr);
Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
std::shared_ptr<Ui::Show> show);
void InitMessageFieldHandlers(
not_null<Main::Session*> session,
std::shared_ptr<Main::SessionShow> show, // may be null
not_null<Ui::InputField*> field,
Fn<bool()> customEmojiPaused,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji = nullptr,
const style::InputField *fieldStyle = nullptr);
struct MessageFieldHandlersArgs {
not_null<Main::Session*> session;
std::shared_ptr<Main::SessionShow> show; // may be null
not_null<Ui::InputField*> field;
Fn<bool()> customEmojiPaused;
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji;
const style::InputField *fieldStyle = nullptr;
base::flat_set<QString> allowMarkdownTags;
};
void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args);
void InitMessageFieldHandlers(
not_null<Window::SessionController*> controller,
not_null<Ui::InputField*> field,

View file

@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/dynamic_image.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/rect.h"
#include "ui/round_rect.h"
#include "styles/style_chat.h"
@ -222,10 +223,15 @@ QMargins MediaGeneric::inBubblePadding() const {
MediaGenericTextPart::MediaGenericTextPart(
TextWithEntities text,
QMargins margins,
const base::flat_map<uint16, ClickHandlerPtr> &links)
const base::flat_map<uint16, ClickHandlerPtr> &links,
const std::any &context)
: _text(st::msgMinWidth)
, _margins(margins) {
_text.setMarkedText(st::defaultTextStyle, text);
_text.setMarkedText(
st::defaultTextStyle,
text,
kMarkupTextOptions,
context);
for (const auto &[index, link] : links) {
_text.setLink(index, link);
}
@ -248,7 +254,10 @@ void MediaGenericTextPart::draw(
.palette = &(service
? context.st->serviceTextPalette()
: context.messageStyle()->textPalette),
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
});
}

View file

@ -121,7 +121,8 @@ public:
MediaGenericTextPart(
TextWithEntities text,
QMargins margins,
const base::flat_map<uint16, ClickHandlerPtr> &links = {});
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
const std::any &context = {});
void draw(
Painter &p,

View file

@ -6,14 +6,20 @@ For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/media/history_view_service_box.h"
//
#include "history/view/history_view_cursor_state.h"
#include "core/ui_integration.h"
#include "history/view/media/history_view_sticker_player_abstract.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_text_helper.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "styles/style_chat.h"
#include "styles/style_premium.h"
#include "styles/style_layers.h"
@ -48,9 +54,15 @@ ServiceBox::ServiceBox(
EntityType::StrikeOut,
EntityType::Underline,
EntityType::Italic,
EntityType::Spoiler,
EntityType::CustomEmoji,
}),
kMarkupTextOptions,
_maxWidth)
_maxWidth,
Core::MarkedTextContext{
.session = &parent->history()->session(),
.customEmojiRepaint = [parent] { parent->customEmojiRepaint(); },
})
, _size(
_content->width(),
(st::msgServiceGiftBoxTopSkip
@ -67,6 +79,7 @@ ServiceBox::ServiceBox(
: (_content->buttonSkip() + st::msgServiceGiftBoxButtonHeight))
+ st::msgServiceGiftBoxButtonMargins.bottom()))
, _innerSize(_size - QSize(0, st::msgServiceGiftBoxTopSkip)) {
InitElementTextPart(_parent, _subtitle);
if (auto text = _content->button()) {
_button.repaint = [=] { repaint(); };
std::move(text) | rpl::start_with_next([=](QString value) {
@ -116,7 +129,17 @@ void ServiceBox::draw(Painter &p, const PaintContext &context) const {
_title.draw(p, st::msgPadding.left(), top, _maxWidth, style::al_top);
top += _title.countHeight(_maxWidth) + padding.bottom();
}
_subtitle.draw(p, st::msgPadding.left(), top, _maxWidth, style::al_top);
_parent->prepareCustomEmojiPaint(p, context, _subtitle);
_subtitle.draw(p, {
.position = QPoint(st::msgPadding.left(), top),
.availableWidth = _maxWidth,
.align = style::al_top,
.palette = &context.st->serviceTextPalette(),
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
});
top += _subtitle.countHeight(_maxWidth) + padding.bottom();
}
@ -180,8 +203,30 @@ void ServiceBox::draw(Painter &p, const PaintContext &context) const {
TextState ServiceBox::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
const auto content = contentRect();
const auto lookupSubtitleLink = [&] {
auto top = st::msgServiceGiftBoxTopSkip
+ content.top()
+ content.height();
const auto &padding = st::msgServiceGiftBoxTitlePadding;
top += padding.top();
if (!_title.isEmpty()) {
top += _title.countHeight(_maxWidth) + padding.bottom();
}
auto subtitleRequest = request.forText();
subtitleRequest.align = style::al_top;
const auto state = _subtitle.getState(
point - QPoint(st::msgPadding.left(), top),
_maxWidth,
subtitleRequest);
if (state.link) {
result.link = state.link;
}
};
if (_button.empty()) {
if (QRect(QPoint(), _innerSize).contains(point)) {
if (!_button.link) {
lookupSubtitleLink();
} else if (QRect(QPoint(), _innerSize).contains(point)) {
result.link = _button.link;
}
} else {
@ -189,11 +234,13 @@ TextState ServiceBox::textState(QPoint point, StateRequest request) const {
if (rect.contains(point)) {
result.link = _button.link;
_button.lastPoint = point - rect.topLeft();
} else if (contentRect().contains(point)) {
} else if (content.contains(point)) {
if (!_contentLink) {
_contentLink = _content->createViewLink();
}
result.link = _contentLink;
} else {
lookupSubtitleLink();
}
}
return result;
@ -238,6 +285,10 @@ bool ServiceBox::customInfoLayout() const {
return false;
}
void ServiceBox::hideSpoilers() {
_subtitle.setSpoilerRevealed(false, anim::type::instant);
}
bool ServiceBox::hasHeavyPart() const {
return _content->hasHeavyPart();
}

View file

@ -81,6 +81,7 @@ public:
[[nodiscard]] bool hideServiceText() const override {
return _content->hideServiceText();
}
void hideSpoilers() override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;

View file

@ -1091,11 +1091,10 @@ void Notification::showReplyField() {
_replyArea->setFocus();
_replyArea->setMaxLength(MaxMessageSize);
_replyArea->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
InitMessageFieldHandlers(
&_item->history()->session(),
nullptr,
_replyArea.data(),
nullptr);
InitMessageFieldHandlers({
.session = &_item->history()->session(),
.field = _replyArea.data(),
});
// Catch mouse press event to activate the window.
QCoreApplication::instance()->installEventFilter(this);

View file

@ -2215,11 +2215,14 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox(
field->submits(
) | rpl::start_with_next([=] { submit({}); }, field->lifetime());
InitMessageFieldHandlers(
session,
show,
field,
[=] { return show->paused(GifPauseReason::Layer); });
InitMessageFieldHandlers({
.session = session,
.show = show,
.field = field,
.customEmojiPaused = [=] {
return show->paused(GifPauseReason::Layer);
},
});
field->setSubmitSettings(Core::App().settings().sendSubmitWay());
Ui::SendPendingMoveResizeEvents(comment);