Implement required paywalls in tags.

This commit is contained in:
John Preston 2024-01-29 11:07:53 +04:00
parent 46579ac84d
commit 11cf0486cb
16 changed files with 242 additions and 60 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

View file

@ -2814,8 +2814,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_read_hidden" = "read"; "lng_context_read_hidden" = "read";
"lng_context_read_show" = "show when"; "lng_context_read_show" = "show when";
"lng_add_tag_about" = "Tag this message with an emoji for quick search.";
"lng_subscribe_tag_about" = "Organize your Saved Messages with tags. {link}";
"lng_subscribe_tag_link" = "Learn More...";
"lng_edit_tag_about" = "You can label your emoji tag with a text name."; "lng_edit_tag_about" = "You can label your emoji tag with a text name.";
"lng_edit_tag_name" = "Name"; "lng_edit_tag_name" = "Name";
"lng_add_tag_button" = "Add tags";
"lng_add_tag_phrase" = "to messages {arrow}";
"lng_add_tag_phrase_long" = "to your Saved Messages {arrow}";
"lng_unlock_tags" = "Unlock";
"lng_context_animated_emoji" = "This message contains emoji from **{name} pack**."; "lng_context_animated_emoji" = "This message contains emoji from **{name} pack**.";
"lng_context_animated_emoji_many#one" = "This message contains emoji from **{count} pack**."; "lng_context_animated_emoji_many#one" = "This message contains emoji from **{count} pack**.";
@ -4471,6 +4478,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_stories_channel_archive_done_many#one" = "{count} story is hidden from the channel page."; "lng_stories_channel_archive_done_many#one" = "{count} story is hidden from the channel page.";
"lng_stories_channel_archive_done_many#other" = "{count} stories are hidden from the channel page."; "lng_stories_channel_archive_done_many#other" = "{count} stories are hidden from the channel page.";
"lng_stories_save_promo" = "Subscribe to {link} to download other people's unprotected stories to disk."; "lng_stories_save_promo" = "Subscribe to {link} to download other people's unprotected stories to disk.";
"lng_stories_reaction_as_message" = "Send reaction as a private message";
"lng_stealth_mode_menu_item" = "Stealth Mode"; "lng_stealth_mode_menu_item" = "Stealth Mode";
"lng_stealth_mode_title" = "Stealth Mode"; "lng_stealth_mode_title" = "Stealth Mode";

View file

@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/confirm_box.h" #include "ui/boxes/confirm_box.h"
#include "boxes/share_box.h" #include "boxes/share_box.h"
#include "boxes/connection_box.h" #include "boxes/connection_box.h"
#include "boxes/premium_preview_box.h"
#include "boxes/sticker_set_box.h" #include "boxes/sticker_set_box.h"
#include "boxes/sessions_box.h" #include "boxes/sessions_box.h"
#include "boxes/language_box.h" #include "boxes/language_box.h"
@ -657,6 +658,17 @@ bool CopyPeerId(
return true; return true;
} }
bool ShowSearchTagsPromo(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
ShowPremiumPreviewBox(controller, PremiumPreview::TagsForMessages);
return true;
}
void ExportTestChatTheme( void ExportTestChatTheme(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
not_null<const Data::CloudTheme*> theme) { not_null<const Data::CloudTheme*> theme) {
@ -1020,6 +1032,10 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
{ {
u"^copy:(.+)$"_q, u"^copy:(.+)$"_q,
CopyPeerId CopyPeerId
},
{
u"about_tags"_q,
ShowSearchTagsPromo
} }
}; };
return Result; return Result;

View file

@ -626,3 +626,7 @@ searchedBarPosition: point(17px, 7px);
dialogsSearchTagSkip: point(8px, 4px); dialogsSearchTagSkip: point(8px, 4px);
dialogsSearchTagBottom: 10px; dialogsSearchTagBottom: 10px;
dialogsSearchTagLocked: icon{{ "emoji/premium_lock", lightButtonFgOver }};
dialogsSearchTagPromo: defaultTextStyle;
dialogsSearchTagArrow: icon{{ "dialogs/tags_arrow", windowSubTextFg }};
dialogsSearchTagArrowPadding: margins(0px, 3px, 0px, 0px);

View file

@ -18,8 +18,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/dialogs_search_tags.h" #include "dialogs/dialogs_search_tags.h"
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "core/shortcuts.h"
#include "core/application.h" #include "core/application.h"
#include "core/click_handler_types.h"
#include "core/shortcuts.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h" #include "ui/widgets/popup_menu.h"
#include "ui/widgets/scroll_area.h" #include "ui/widgets/scroll_area.h"
@ -1783,7 +1784,11 @@ void InnerWidget::mousePressReleased(
} }
} }
if (auto activated = ClickHandler::unpressed()) { if (auto activated = ClickHandler::unpressed()) {
ActivateClickHandler(window(), activated, ClickContext{ button }); ActivateClickHandler(window(), activated, ClickContext{
button,
QVariant::fromValue(ClickHandlerContext{
.sessionWindow = _controller,
}) });
} }
} }
@ -3002,8 +3007,8 @@ void InnerWidget::searchInChat(
update(0, searchInChatOffset(), width(), height); update(0, searchInChatOffset(), width(), height);
}, _searchTags->lifetime()); }, _searchTags->lifetime());
_searchTags->heightValue() | rpl::filter( _searchTags->heightValue() | rpl::skip(
rpl::mappers::_1 > 0 1
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
refresh(); refresh();
moveCancelSearchButtons(); moveCancelSearchButtons();

View file

@ -8,13 +8,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/dialogs_search_tags.h" #include "dialogs/dialogs_search_tags.h"
#include "base/qt/qt_key_modifiers.h" #include "base/qt/qt_key_modifiers.h"
#include "boxes/premium_preview_box.h"
#include "core/click_handler_types.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_custom_emoji.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
#include "data/data_peer_values.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "history/view/reactions/history_view_reactions.h" #include "history/view/reactions/history_view_reactions.h"
#include "main/main_session.h"
#include "lang/lang_keys.h"
#include "ui/effects/animation_value.h" #include "ui/effects/animation_value.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/power_saving.h" #include "ui/power_saving.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_dialogs.h" #include "styles/style_dialogs.h"
@ -32,6 +41,45 @@ namespace {
return TextUtilities::SingleLine(result); return TextUtilities::SingleLine(result);
} }
[[nodiscard]] ClickHandlerPtr MakePromoLink() {
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
ShowPremiumPreviewBox(
controller,
PremiumPreview::TagsForMessages);
}
});
}
[[nodiscard]] Ui::Text::String FillAdditionalText(
not_null<Data::Session*> owner,
int width) {
auto emoji = Ui::Text::SingleCustomEmoji(
owner->customEmojiManager().registerInternalEmoji(
st::dialogsSearchTagArrow,
st::dialogsSearchTagArrowPadding));
auto result = Ui::Text::String();
const auto context = Core::MarkedTextContext{
.session = &owner->session(),
.customEmojiRepaint = [] {},
.customEmojiLoopLimit = 1,
};
const auto attempt = [&](const auto &phrase) {
result.setMarkedText(
st::dialogsSearchTagPromo,
phrase(tr::now, lt_arrow, emoji, Ui::Text::WithEntities),
kMarkupTextOptions,
context);
return result.maxWidth() < width;
};
if (attempt(tr::lng_add_tag_phrase_long)
|| attempt(tr::lng_add_tag_phrase)) {
return result;
}
return {};
}
} // namespace } // namespace
struct SearchTags::Tag { struct SearchTags::Tag {
@ -43,6 +91,7 @@ struct SearchTags::Tag {
QRect geometry; QRect geometry;
ClickHandlerPtr link; ClickHandlerPtr link;
bool selected = false; bool selected = false;
bool promo = false;
}; };
SearchTags::SearchTags( SearchTags::SearchTags(
@ -51,10 +100,13 @@ SearchTags::SearchTags(
std::vector<Data::ReactionId> selected) std::vector<Data::ReactionId> selected)
: _owner(owner) : _owner(owner)
, _added(selected) { , _added(selected) {
std::move( rpl::combine(
tags std::move(tags),
) | rpl::start_with_next([=](const std::vector<Data::Reaction> &list) { Data::AmPremiumValue(&owner->session())
fill(list); ) | rpl::start_with_next([=](
const std::vector<Data::Reaction> &list,
bool premium) {
fill(list, premium);
}, _lifetime); }, _lifetime);
// Mark the `selected` reactions as selected in `_tags`. // Mark the `selected` reactions as selected in `_tags`.
@ -73,12 +125,19 @@ SearchTags::SearchTags(
SearchTags::~SearchTags() = default; SearchTags::~SearchTags() = default;
void SearchTags::fill(const std::vector<Data::Reaction> &list) { void SearchTags::fill(
const std::vector<Data::Reaction> &list,
bool premium) {
const auto selected = collectSelected(); const auto selected = collectSelected();
_tags.clear(); _tags.clear();
_tags.reserve(list.size()); _tags.reserve(list.size());
const auto link = [&](Data::ReactionId id) { const auto link = [&](Data::ReactionId id) {
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] { return std::make_shared<LambdaClickHandler>(crl::guard(this, [=](
ClickContext context) {
if (!premium) {
MakePromoLink()->onClick(context);
return;
}
const auto i = ranges::find(_tags, id, &Tag::id); const auto i = ranges::find(_tags, id, &Tag::id);
if (i != end(_tags)) { if (i != end(_tags)) {
if (!i->selected && !base::IsShiftPressed()) { if (!i->selected && !base::IsShiftPressed()) {
@ -109,6 +168,18 @@ void SearchTags::fill(const std::vector<Data::Reaction> &list) {
_owner->reactions().preloadImageFor(id); _owner->reactions().preloadImageFor(id);
} }
}; };
if (!premium) {
const auto text = (list.empty() && _added.empty())
? tr::lng_add_tag_button(tr::now)
: tr::lng_unlock_tags(tr::now);
_tags.push_back({
.id = Data::ReactionId(),
.text = text,
.textWidth = st::reactionInlineTagFont->width(text),
.link = MakePromoLink(),
.promo = true,
});
}
for (const auto &reaction : list) { for (const auto &reaction : list) {
if (reaction.count > 0 if (reaction.count > 0
|| ranges::contains(_added, reaction.id) || ranges::contains(_added, reaction.id)
@ -131,10 +202,11 @@ void SearchTags::layout() {
Expects(_width > 0); Expects(_width > 0);
if (_tags.empty()) { if (_tags.empty()) {
_additionalText = {};
_height = 0; _height = 0;
return; return;
} }
const auto &bg = validateBg(false); const auto &bg = validateBg(false, false);
const auto skip = st::dialogsSearchTagSkip; const auto skip = st::dialogsSearchTagSkip;
const auto size = bg.size() / bg.devicePixelRatio(); const auto size = bg.size() / bg.devicePixelRatio();
const auto xbase = size.width(); const auto xbase = size.width();
@ -147,10 +219,17 @@ void SearchTags::layout() {
x = 0; x = 0;
y += ybase + skip.y(); y += ybase + skip.y();
} }
tag.geometry = QRect(x, y, width, xbase); tag.geometry = QRect(x, y, width, ybase);
x += width + skip.x(); x += width + skip.x();
} }
_height = y + ybase + st::dialogsSearchTagBottom; _height = y + ybase + st::dialogsSearchTagBottom;
if (_tags.size() == 1 && _tags.front().promo) {
_additionalLeft = x;
const auto additionalWidth = _width - _additionalLeft;
_additionalText = FillAdditionalText(_owner, additionalWidth);
} else {
_additionalText = {};
}
} }
void SearchTags::resizeToWidth(int width) { void SearchTags::resizeToWidth(int width) {
@ -177,6 +256,14 @@ ClickHandlerPtr SearchTags::lookupHandler(QPoint point) const {
for (const auto &tag : _tags) { for (const auto &tag : _tags) {
if (tag.geometry.contains(point.x(), point.y())) { if (tag.geometry.contains(point.x(), point.y())) {
return tag.link; return tag.link;
} else if (tag.promo
&& !_additionalText.isEmpty()
&& tag.geometry.united(QRect(
_additionalLeft,
tag.geometry.y(),
_additionalText.maxWidth(),
tag.geometry.height())).contains(point.x(), point.y())) {
return tag.link;
} }
} }
return nullptr; return nullptr;
@ -227,7 +314,7 @@ void SearchTags::paintCustomFrame(
} }
void SearchTags::paint( void SearchTags::paint(
QPainter &p, Painter &p,
QPoint position, QPoint position,
crl::time now, crl::time now,
bool paused) const { bool paused) const {
@ -236,9 +323,9 @@ void SearchTags::paint(
const auto padding = st::reactionInlinePadding; const auto padding = st::reactionInlinePadding;
for (const auto &tag : _tags) { for (const auto &tag : _tags) {
const auto geometry = tag.geometry.translated(position); const auto geometry = tag.geometry.translated(position);
paintBackground(p, geometry, tag.selected); paintBackground(p, geometry, tag);
paintText(p, geometry, tag); paintText(p, geometry, tag);
if (!tag.custom && tag.image.isNull()) { if (!tag.custom && !tag.promo && tag.image.isNull()) {
tag.image = _owner->reactions().resolveImageFor( tag.image = _owner->reactions().resolveImageFor(
tag.id, tag.id,
::Data::Reactions::ImageSize::InlineList); ::Data::Reactions::ImageSize::InlineList);
@ -247,7 +334,9 @@ void SearchTags::paint(
const auto image = QRect( const auto image = QRect(
inner.topLeft() + QPoint(skip, skip), inner.topLeft() + QPoint(skip, skip),
QSize(st::reactionInlineImage, st::reactionInlineImage)); QSize(st::reactionInlineImage, st::reactionInlineImage));
if (const auto custom = tag.custom.get()) { if (tag.promo) {
st::dialogsSearchTagLocked.paintInCenter(p, image);
} else if (const auto custom = tag.custom.get()) {
const auto textFg = tag.selected const auto textFg = tag.selected
? st::dialogsNameFgActive->c ? st::dialogsNameFgActive->c
: st::dialogsNameFgOver->c; : st::dialogsNameFgOver->c;
@ -262,13 +351,26 @@ void SearchTags::paint(
p.drawImage(image.topLeft(), tag.image); p.drawImage(image.topLeft(), tag.image);
} }
} }
paintAdditionalText(p, position);
}
void SearchTags::paintAdditionalText(Painter &p, QPoint position) const {
if (_additionalText.isEmpty()) {
return;
}
const auto x = position.x() + _additionalLeft;
const auto tag = _tags.front().geometry;
const auto height = st::dialogsSearchTagPromo.font->height;
const auto y = position.y() + tag.y() + (tag.height() - height) / 2;
p.setPen(st::windowSubTextFg);
_additionalText.drawLeft(p, x, y, _width - x, _width);
} }
void SearchTags::paintBackground( void SearchTags::paintBackground(
QPainter &p, QPainter &p,
QRect geometry, QRect geometry,
bool selected) const { const Tag &tag) const {
const auto &image = validateBg(selected); const auto &image = validateBg(tag.selected, tag.promo);
const auto ratio = int(image.devicePixelRatio()); const auto ratio = int(image.devicePixelRatio());
const auto size = image.size() / ratio; const auto size = image.size() / ratio;
if (const auto fill = geometry.width() - size.width(); fill > 0) { if (const auto fill = geometry.width() - size.width(); fill > 0) {
@ -282,7 +384,7 @@ void SearchTags::paintBackground(
QRect(QPoint(), QSize(left, size.height()) * ratio)); QRect(QPoint(), QSize(left, size.height()) * ratio));
p.fillRect( p.fillRect(
QRect(x + left, y, fill, size.height()), QRect(x + left, y, fill, size.height()),
bgColor(selected)); bgColor(tag.selected, tag.promo));
p.drawImage( p.drawImage(
QRect(x + left + fill, y, right, size.height()), QRect(x + left + fill, y, right, size.height()),
image, image,
@ -292,30 +394,39 @@ void SearchTags::paintBackground(
} }
} }
void SearchTags::paintText(QPainter &p, QRect geometry, const Tag &tag) const { void SearchTags::paintText(
QPainter &p,
QRect geometry,
const Tag &tag) const {
using namespace HistoryView::Reactions; using namespace HistoryView::Reactions;
if (tag.text.isEmpty()) { if (tag.text.isEmpty()) {
return; return;
} }
p.setPen(tag.selected ? st::dialogsTextFgActive : st::windowSubTextFg); p.setPen(tag.promo
? st::lightButtonFgOver
: tag.selected
? st::dialogsTextFgActive
: st::windowSubTextFg);
p.setFont(st::reactionInlineTagFont); p.setFont(st::reactionInlineTagFont);
const auto x = geometry.x() + st::reactionInlineTagNamePosition.x(); const auto x = geometry.x() + st::reactionInlineTagNamePosition.x();
const auto y = geometry.y() + st::reactionInlineTagNamePosition.y(); const auto y = geometry.y() + st::reactionInlineTagNamePosition.y();
p.drawText(x, y + st::reactionInlineTagFont->ascent, tag.text); p.drawText(x, y + st::reactionInlineTagFont->ascent, tag.text);
} }
QColor SearchTags::bgColor(bool selected) const { QColor SearchTags::bgColor(bool selected, bool promo) const {
return selected return promo
? st::lightButtonBgOver->c
: selected
? st::dialogsBgActive->c ? st::dialogsBgActive->c
: st::dialogsBgOver->c; : st::dialogsBgOver->c;
} }
const QImage &SearchTags::validateBg(bool selected) const { const QImage &SearchTags::validateBg(bool selected, bool promo) const {
using namespace HistoryView::Reactions; using namespace HistoryView::Reactions;
auto &image = selected ? _selectedBg : _normalBg; auto &image = promo ? _promoBg : selected ? _selectedBg : _normalBg;
if (image.isNull()) { if (image.isNull()) {
const auto tagBg = bgColor(selected); const auto tagBg = bgColor(selected, promo);
const auto dotBg = st::transparent->c; const auto dotBg = st::transparent->c;
image = InlineList::PrepareTagBg(tagBg, dotBg); image = InlineList::PrepareTagBg(tagBg, dotBg);
} }

View file

@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/weak_ptr.h" #include "base/weak_ptr.h"
class Painter;
namespace Data { namespace Data {
class Session; class Session;
struct Reaction; struct Reaction;
@ -39,7 +41,7 @@ public:
-> rpl::producer<std::vector<Data::ReactionId>>; -> rpl::producer<std::vector<Data::ReactionId>>;
void paint( void paint(
QPainter &p, Painter &p,
QPoint position, QPoint position,
crl::time now, crl::time now,
bool paused) const; bool paused) const;
@ -49,7 +51,7 @@ public:
private: private:
struct Tag; struct Tag;
void fill(const std::vector<Data::Reaction> &list); void fill(const std::vector<Data::Reaction> &list, bool premium);
void paintCustomFrame( void paintCustomFrame(
QPainter &p, QPainter &p,
not_null<Ui::Text::CustomEmoji*> emoji, not_null<Ui::Text::CustomEmoji*> emoji,
@ -59,25 +61,26 @@ private:
const QColor &textColor) const; const QColor &textColor) const;
void layout(); void layout();
[[nodiscard]] std::vector<Data::ReactionId> collectSelected() const; [[nodiscard]] std::vector<Data::ReactionId> collectSelected() const;
[[nodiscard]] QColor bgColor(bool selected) const; [[nodiscard]] QColor bgColor(bool selected, bool promo) const;
[[nodiscard]] const QImage &validateBg(bool selected) const; [[nodiscard]] const QImage &validateBg(bool selected, bool promo) const;
void paintBackground( void paintAdditionalText(Painter &p, QPoint position) const;
QPainter &p, void paintBackground(QPainter &p, QRect geometry, const Tag &tag) const;
QRect geometry,
bool selected) const;
void paintText(QPainter &p, QRect geometry, const Tag &tag) const; void paintText(QPainter &p, QRect geometry, const Tag &tag) const;
const not_null<Data::Session*> _owner; const not_null<Data::Session*> _owner;
std::vector<Data::ReactionId> _added; std::vector<Data::ReactionId> _added;
std::vector<Tag> _tags; std::vector<Tag> _tags;
Ui::Text::String _additionalText;
rpl::event_stream<> _selectedChanges; rpl::event_stream<> _selectedChanges;
rpl::event_stream<> _repaintRequests; rpl::event_stream<> _repaintRequests;
mutable QImage _normalBg; mutable QImage _normalBg;
mutable QImage _selectedBg; mutable QImage _selectedBg;
mutable QImage _promoBg;
mutable QImage _customCache; mutable QImage _customCache;
mutable int _customSkip = 0; mutable int _customSkip = 0;
rpl::variable<int> _height; rpl::variable<int> _height;
int _width = 0; int _width = 0;
int _additionalLeft = 0;
rpl::lifetime _lifetime; rpl::lifetime _lifetime;

View file

@ -2636,9 +2636,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
desiredPosition, desiredPosition,
reactItem, reactItem,
[=](ChosenReaction reaction) { reactionChosen(reaction); }, [=](ChosenReaction reaction) { reactionChosen(reaction); },
(reactItem->reactionsAreTags() ItemReactionsAbout(reactItem),
? TextWithEntities{ u"Organize your Saved Messages with tags. Learn More..."_q }
: TextWithEntities()),
_controller->cachedReactionIconFactory().createMethod()) _controller->cachedReactionIconFactory().createMethod())
: AttachSelectorResult::Skipped; : AttachSelectorResult::Skipped;
if (attached == AttachSelectorResult::Failed) { if (attached == AttachSelectorResult::Failed) {

View file

@ -2633,7 +2633,7 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
desiredPosition, desiredPosition,
reactItem, reactItem,
[=](ChosenReaction reaction) { reactionChosen(reaction); }, [=](ChosenReaction reaction) { reactionChosen(reaction); },
TextWithEntities(), ItemReactionsAbout(reactItem),
_controller->cachedReactionIconFactory().createMethod()) _controller->cachedReactionIconFactory().createMethod())
: AttachSelectorResult::Skipped; : AttachSelectorResult::Skipped;
if (attached == AttachSelectorResult::Failed) { if (attached == AttachSelectorResult::Failed) {

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_reply.h" #include "history/view/history_view_reply.h"
#include "history/view/history_view_view_button.h" // ViewButton. #include "history/view/history_view_view_button.h" // ViewButton.
#include "history/history.h" #include "history/history.h"
#include "boxes/premium_preview_box.h"
#include "boxes/share_box.h" #include "boxes/share_box.h"
#include "ui/effects/glare.h" #include "ui/effects/glare.h"
#include "ui/effects/reaction_fly_animation.h" #include "ui/effects/reaction_fly_animation.h"
@ -49,13 +50,13 @@ namespace {
constexpr auto kPlayStatusLimit = 2; constexpr auto kPlayStatusLimit = 2;
const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_"; const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_";
[[nodiscard]] std::optional<Window::SessionController*> ExtractController( [[nodiscard]] Window::SessionController *ExtractController(
const ClickContext &context) { const ClickContext &context) {
const auto my = context.other.value<ClickHandlerContext>(); const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) { if (const auto controller = my.sessionWindow.get()) {
return controller; return controller;
} }
return std::nullopt; return nullptr;
} }
class KeyboardStyle : public ReplyKeyboard::Style { class KeyboardStyle : public ReplyKeyboard::Style {
@ -2288,11 +2289,8 @@ ClickHandlerPtr Message::createGoToCommentsLink() const {
const auto fullId = data()->fullId(); const auto fullId = data()->fullId();
const auto sessionId = data()->history()->session().uniqueId(); const auto sessionId = data()->history()->session().uniqueId();
return std::make_shared<LambdaClickHandler>([=](ClickContext context) { return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto controller = ExtractController(context).value_or(nullptr); const auto controller = ExtractController(context);
if (!controller) { if (!controller || controller->session().uniqueId() != sessionId) {
return;
}
if (controller->session().uniqueId() != sessionId) {
return; return;
} }
if (const auto item = controller->session().data().message(fullId)) { if (const auto item = controller->session().data().message(fullId)) {
@ -2984,12 +2982,20 @@ void Message::refreshReactions() {
return std::make_shared<LambdaClickHandler>([=]( return std::make_shared<LambdaClickHandler>([=](
ClickContext context) { ClickContext context) {
if (const auto strong = weak.get()) { if (const auto strong = weak.get()) {
if (strong->data()->reactionsAreTags()) { const auto item = strong->data();
if (item->reactionsAreTags()) {
if (item->history()->session().premium()) {
const auto tag = Data::SearchTagToQuery(id); const auto tag = Data::SearchTagToQuery(id);
HashtagClickHandler(tag).onClick(context); HashtagClickHandler(tag).onClick(context);
} else if (const auto controller
= ExtractController(context)) {
ShowPremiumPreviewBox(
controller,
PremiumPreview::TagsForMessages);
}
return; return;
} }
strong->data()->toggleReaction( item->toggleReaction(
id, id,
HistoryItem::ReactionSource::Existing); HistoryItem::ReactionSource::Existing);
if (const auto now = weak.get()) { if (const auto now = weak.get()) {
@ -3586,11 +3592,8 @@ ClickHandlerPtr Message::prepareRightActionLink() const {
}; };
return std::make_shared<LambdaClickHandler>([=]( return std::make_shared<LambdaClickHandler>([=](
ClickContext context) { ClickContext context) {
const auto controller = ExtractController(context).value_or(nullptr); const auto controller = ExtractController(context);
if (!controller) { if (!controller || controller->session().uniqueId() != sessionId) {
return;
}
if (controller->session().uniqueId() != sessionId) {
return; return;
} }

View file

@ -12,13 +12,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/popup_menu.h" #include "ui/widgets/popup_menu.h"
#include "ui/widgets/shadow.h" #include "ui/widgets/shadow.h"
#include "ui/text/text_custom_emoji.h" #include "ui/text/text_custom_emoji.h"
#include "ui/text/text_utilities.h"
#include "ui/platform/ui_platform_utility.h" #include "ui/platform/ui_platform_utility.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_document_media.h" #include "data/data_document_media.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_custom_emoji.h"
#include "lang/lang_keys.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "chat_helpers/emoji_list_widget.h" #include "chat_helpers/emoji_list_widget.h"
#include "chat_helpers/stickers_list_footer.h" #include "chat_helpers/stickers_list_footer.h"
@ -277,6 +280,13 @@ Selector::Selector(
, _skipy((st::reactStripHeight - st::reactStripSize) / 2) { , _skipy((st::reactStripHeight - st::reactStripSize) / 2) {
setMouseTracking(true); setMouseTracking(true);
if (_about) {
_about->setClickHandlerFilter([=](const auto &...) {
_escapes.fire({});
return true;
});
}
_useTransparency = child || Ui::Platform::TranslucentWindowsSupported(); _useTransparency = child || Ui::Platform::TranslucentWindowsSupported();
} }
@ -1173,6 +1183,10 @@ AttachSelectorResult AttachSelectorToMenu(
chosen(std::move(reaction)); chosen(std::move(reaction));
}, selector->lifetime()); }, selector->lifetime());
selector->escapes() | rpl::start_with_next([=] {
menu->hideMenu();
}, selector->lifetime());
const auto weak = base::make_weak(controller); const auto weak = base::make_weak(controller);
controller->enableGifPauseReason( controller->enableGifPauseReason(
Window::GifPauseReason::MediaPreview); Window::GifPauseReason::MediaPreview);
@ -1249,4 +1263,18 @@ auto AttachSelectorToMenu(
return selector; return selector;
} }
TextWithEntities ItemReactionsAbout(not_null<HistoryItem*> item) {
return !item->reactionsAreTags()
? TextWithEntities()
: item->history()->session().premium()
? TextWithEntities{ tr::lng_add_tag_about(tr::now) }
: tr::lng_subscribe_tag_about(
tr::now,
lt_link,
Ui::Text::Link(
tr::lng_subscribe_tag_link(tr::now),
u"internal:about_tags"_q),
Ui::Text::WithEntities);
}
} // namespace HistoryView::Reactions } // namespace HistoryView::Reactions

View file

@ -265,4 +265,7 @@ AttachSelectorResult AttachSelectorToMenu(
IconFactory iconFactory IconFactory iconFactory
) -> base::expected<not_null<Selector*>, AttachSelectorResult>; ) -> base::expected<not_null<Selector*>, AttachSelectorResult>;
[[nodiscard]] TextWithEntities ItemReactionsAbout(
not_null<HistoryItem*> item);
} // namespace HistoryView::Reactions } // namespace HistoryView::Reactions

View file

@ -27,7 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h" #include "history/history.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "media/stories/media_stories_controller.h" #include "media/stories/media_stories_controller.h"
#include "lang/lang_tag.h" #include "lang/lang_keys.h"
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
#include "ui/effects/emoji_fly_animation.h" #include "ui/effects/emoji_fly_animation.h"
#include "ui/effects/path_shift_gradient.h" #include "ui/effects/path_shift_gradient.h"
@ -676,7 +676,7 @@ void Reactions::Panel::create() {
_controller->uiShow(), _controller->uiShow(),
std::move(reactions), std::move(reactions),
TextWithEntities{ (mode == Mode::Message TextWithEntities{ (mode == Mode::Message
? u"Send reaction as a private message"_q ? tr::lng_stories_reaction_as_message(tr::now)
: QString()) }, : QString()) },
_controller->cachedReactionIconFactory().createMethod(), _controller->cachedReactionIconFactory().createMethod(),
[=](bool fast) { hide(mode); }); [=](bool fast) { hide(mode); });

View file

@ -525,13 +525,16 @@ bool ShowReactPremiumError(
not_null<SessionController*> controller, not_null<SessionController*> controller,
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
const Data::ReactionId &id) { const Data::ReactionId &id) {
if (controller->session().premium() if (item->reactionsAreTags()) {
if (controller->session().premium()) {
return false;
}
ShowPremiumPreviewBox(controller, PremiumPreview::TagsForMessages);
return true;
} else if (controller->session().premium()
|| ranges::contains(item->chosenReactions(), id) || ranges::contains(item->chosenReactions(), id)
|| item->history()->peer->isBroadcast()) { || item->history()->peer->isBroadcast()) {
return false; return false;
} else if (item->reactionsAreTags()) {
ShowPremiumPreviewBox(controller, PremiumPreview::TagsForMessages);
return true;
} else if (!id.custom()) { } else if (!id.custom()) {
return false; return false;
} }