Implement rich reactions selector.

This commit is contained in:
John Preston 2023-11-14 14:35:57 +04:00
parent 1e26c33b3d
commit 43a8733fc7
17 changed files with 718 additions and 214 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1347,6 +1347,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_reactions_none_about" = "Members of the group can't add any reactions to messages."; "lng_manage_peer_reactions_none_about" = "Members of the group can't add any reactions to messages.";
"lng_manage_peer_reactions_some_title" = "Only allow these reactions"; "lng_manage_peer_reactions_some_title" = "Only allow these reactions";
"lng_manage_peer_reactions_available" = "Available reactions"; "lng_manage_peer_reactions_available" = "Available reactions";
"lng_manage_peer_reactions_own" = "You can also {link} emoji packs and use them as reactions.";
"lng_manage_peer_reactions_own_link" = "create your own";
"lng_manage_peer_reactions_level#one" = "Your channel needs to reach level **{count}** to use **{same_count}** custom reaction.";
"lng_manage_peer_reactions_level#other" = "Your channel needs to reach level **{count}** to use **{same_count}** custom reactions.";
"lng_manage_peer_reactions_boost" = "Boost your channel {link}.";
"lng_manage_peer_reactions_boost_link" = "here";
"lng_manage_peer_antispam" = "Aggressive Anti-Spam"; "lng_manage_peer_antispam" = "Aggressive Anti-Spam";
"lng_manage_peer_antispam_about" = "Telegram will filter more spam but may occasionally affect ordinary messages. You can report False Positives in Recent Actions."; "lng_manage_peer_antispam_about" = "Telegram will filter more spam but may occasionally affect ordinary messages. You can report False Positives in Recent Actions.";
@ -2087,6 +2093,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boost_channel_title_color" = "Enable colors"; "lng_boost_channel_title_color" = "Enable colors";
"lng_boost_channel_needs_level_color#one" = "Your channel needs to reach **Level {count}** to change channel color."; "lng_boost_channel_needs_level_color#one" = "Your channel needs to reach **Level {count}** to change channel color.";
"lng_boost_channel_needs_level_color#other" = "Your channel needs to reach **Level {count}** to change channel color."; "lng_boost_channel_needs_level_color#other" = "Your channel needs to reach **Level {count}** to change channel color.";
"lng_boost_channel_title_reactions" = "Custom reactions";
"lng_boost_channel_needs_level_reactions#one" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as a reaction.";
"lng_boost_channel_needs_level_reactions#other" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as reactions.";
"lng_boost_channel_ask" = "Ask your **Premium** subscribers to boost your channel with this link:"; "lng_boost_channel_ask" = "Ask your **Premium** subscribers to boost your channel with this link:";
"lng_boost_channel_ask_button" = "Copy Link"; "lng_boost_channel_ask_button" = "Copy Link";
"lng_boost_channel_or" = "or"; "lng_boost_channel_or" = "or";

View file

@ -527,7 +527,7 @@ void Apply(
show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{ show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{
.link = qs(data.vboost_url()), .link = qs(data.vboost_url()),
.boost = counters, .boost = counters,
.requiredLevel = required, .reason = { Ui::AskBoostChannelColor{ required } },
}, openStatistics, nullptr)); }, openStatistics, nullptr));
cancel(); cancel();
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {

View file

@ -22,6 +22,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/edit_linked_chat_box.h" #include "boxes/peers/edit_linked_chat_box.h"
#include "boxes/peers/edit_peer_requests_box.h" #include "boxes/peers/edit_peer_requests_box.h"
#include "boxes/peers/edit_peer_reactions.h" #include "boxes/peers/edit_peer_reactions.h"
#include "boxes/peers/replace_boost_box.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/stickers_box.h" #include "boxes/stickers_box.h"
#include "boxes/username_box.h" #include "boxes/username_box.h"
#include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/emoji_suggestions_widget.h"
@ -36,12 +38,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer_values.h" #include "data/data_peer_values.h"
#include "data/data_user.h" #include "data/data_user.h"
#include "history/admin_log/history_admin_log_section.h" #include "history/admin_log/history_admin_log_section.h"
#include "info/boosts/info_boosts_widget.h"
#include "info/profile/info_profile_values.h" #include "info/profile/info_profile_values.h"
#include "info/info_memento.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "mtproto/sender.h" #include "mtproto/sender.h"
#include "main/main_account.h" #include "main/main_account.h"
#include "main/main_app_config.h" #include "main/main_app_config.h"
#include "settings/settings_common.h" // IconDescriptor. #include "settings/settings_common.h"
#include "ui/boxes/boost_box.h"
#include "ui/controls/userpic_button.h" #include "ui/controls/userpic_button.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
#include "ui/vertical_list.h" #include "ui/vertical_list.h"
@ -255,6 +260,7 @@ private:
Ui::VerticalLayout *buttonsLayout = nullptr; Ui::VerticalLayout *buttonsLayout = nullptr;
Ui::SettingsButton *forumToggle = nullptr; Ui::SettingsButton *forumToggle = nullptr;
bool forumToggleLocked = false; bool forumToggleLocked = false;
bool levelRequested = false;
Ui::SlideWrap<> *historyVisibilityWrap = nullptr; Ui::SlideWrap<> *historyVisibilityWrap = nullptr;
}; };
struct Saving { struct Saving {
@ -304,6 +310,7 @@ private:
void submitDescription(); void submitDescription();
void deleteWithConfirmation(); void deleteWithConfirmation();
void deleteChannel(); void deleteChannel();
void editReactions();
[[nodiscard]] std::optional<Saving> validate() const; [[nodiscard]] std::optional<Saving> validate() const;
[[nodiscard]] bool validateUsernamesOrder(Saving &to) const; [[nodiscard]] bool validateUsernamesOrder(Saving &to) const;
@ -1100,36 +1107,21 @@ void Controller::fillManageSection() {
return Data::PeerAllowedReactions(peer); return Data::PeerAllowedReactions(peer);
}); });
}) | rpl::flatten_latest(); }) | rpl::flatten_latest();
auto label = rpl::combine( auto label = std::move(
std::move(allowedReactions), allowedReactions
Info::Profile::FullReactionsCountValue(session) ) | rpl::map([=](const Data::AllowedReactions &allowed) {
) | rpl::map([=](const Data::AllowedReactions &allowed, int total) {
const auto some = int(allowed.some.size()); const auto some = int(allowed.some.size());
return (allowed.type != Data::AllowedReactionsType::Some) return (allowed.type != Data::AllowedReactionsType::Some)
? tr::lng_manage_peer_reactions_on(tr::now) ? tr::lng_manage_peer_reactions_on(tr::now)
: some : some
? (QString::number(some) ? QString::number(some)
+ " / "
+ QString::number(std::max(some, total)))
: tr::lng_manage_peer_reactions_off(tr::now); : tr::lng_manage_peer_reactions_off(tr::now);
}); });
const auto done = [=](const Data::AllowedReactions &chosen) {
SaveAllowedReactions(_peer, chosen);
};
AddButtonWithCount( AddButtonWithCount(
_controls.buttonsLayout, _controls.buttonsLayout,
tr::lng_manage_peer_reactions(), tr::lng_manage_peer_reactions(),
std::move(label), std::move(label),
[=] { [=] { editReactions(); },
_navigation->parentController()->show(Box(
EditAllowedReactionsBox,
_navigation,
!_peer->isBroadcast(),
session->data().reactions().list(
Data::Reactions::Type::Active),
Data::PeerAllowedReactions(_peer),
done));
},
{ &st::menuIconGroupReactions }); { &st::menuIconGroupReactions });
} }
if (canEditPermissions) { if (canEditPermissions) {
@ -1277,6 +1269,61 @@ void Controller::fillManageSection() {
} }
} }
void Controller::editReactions() {
const auto done = [=](const Data::AllowedReactions &chosen) {
SaveAllowedReactions(_peer, chosen);
};
if (!_peer->isBroadcast()) {
_navigation->uiShow()->show(Box(
EditAllowedReactionsBox,
EditAllowedReactionsArgs{
.navigation = _navigation,
.isGroup = true,
.list = _navigation->session().data().reactions().list(
Data::Reactions::Type::Active),
.allowed = Data::PeerAllowedReactions(_peer),
.save = done,
}));
return;
}
if (_controls.levelRequested) {
return;
}
_controls.levelRequested = true;
_api.request(MTPpremium_GetBoostsStatus(
_peer->input
)).done([=](const MTPpremium_BoostsStatus &result) {
_controls.levelRequested = false;
const auto link = qs(result.data().vboost_url());
const auto weak = base::make_weak(_navigation->parentController());
auto counters = ParseBoostCounters(result);
counters.mine = 0; // Don't show current level as just-reached.
const auto askForBoosts = [=](int required) {
if (const auto strong = weak.get()) {
const auto openStatistics = [=, peer = _peer] {
strong->showSection(Info::Boosts::Make(peer));
};
strong->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{
.link = link,
.boost = counters,
.reason = { Ui::AskBoostCustomReactions{ required } },
}, openStatistics, nullptr));
}
};
_navigation->uiShow()->show(Box(
EditAllowedReactionsBox,
EditAllowedReactionsArgs{
.navigation = _navigation,
.allowedCustomReactions = counters.level,
.list = _navigation->session().data().reactions().list(
Data::Reactions::Type::Active),
.allowed = Data::PeerAllowedReactions(_peer),
.askForBoosts = askForBoosts,
.save = done,
}));
}).send();
}
void Controller::fillPendingRequestsButton() { void Controller::fillPendingRequestsButton() {
auto pendingRequestsCount = Info::Profile::MigratedOrMeValue( auto pendingRequestsCount = Info::Profile::MigratedOrMeValue(
_peer _peer

View file

@ -15,13 +15,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_chat.h" #include "data/data_chat.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "history/view/reactions/history_view_reactions_selector.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "apiwrap.h" #include "apiwrap.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "ui/boxes/boost_box.h"
#include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/input_field.h"
#include "ui/controls/emoji_button.h"
#include "ui/layers/generic_box.h" #include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h" #include "ui/wrap/slide_wrap.h"
@ -30,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
#include "styles/style_info.h" #include "styles/style_info.h"
#include "styles/style_settings.h" #include "styles/style_settings.h"
#include "styles/style_layers.h"
#include <QtWidgets/QTextEdit> #include <QtWidgets/QTextEdit>
#include <QtGui/QTextBlock> #include <QtGui/QTextBlock>
@ -37,6 +41,70 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace { namespace {
constexpr auto kDisabledEmojiOpacity = 0.4;
struct UniqueCustomEmojiContext {
std::vector<DocumentId> ids;
};
class MaybeDisabledEmoji final : public Ui::Text::CustomEmoji {
public:
MaybeDisabledEmoji(
std::unique_ptr<CustomEmoji> wrapped,
Fn<bool()> enabled);
int width() override;
QString entityData() override;
void paint(QPainter &p, const Context &context) override;
void unload() override;
bool ready() override;
bool readyInDefaultState() override;
private:
const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
const Fn<bool()> _enabled;
};
MaybeDisabledEmoji::MaybeDisabledEmoji(
std::unique_ptr<CustomEmoji> wrapped,
Fn<bool()> enabled)
: _wrapped(std::move(wrapped))
, _enabled(std::move(enabled)) {
}
int MaybeDisabledEmoji::width() {
return _wrapped->width();
}
QString MaybeDisabledEmoji::entityData() {
return _wrapped->entityData();
}
void MaybeDisabledEmoji::paint(QPainter &p, const Context &context) {
const auto disabled = !_enabled();
const auto was = disabled ? p.opacity() : 1.;
if (disabled) {
p.setOpacity(kDisabledEmojiOpacity);
}
_wrapped->paint(p, context);
if (disabled) {
p.setOpacity(was);
}
}
void MaybeDisabledEmoji::unload() {
_wrapped->unload();
}
bool MaybeDisabledEmoji::ready() {
return _wrapped->ready();
}
bool MaybeDisabledEmoji::readyInDefaultState() {
return _wrapped->readyInDefaultState();
}
[[nodiscard]] QString AllowOnlyCustomEmojiProcessor(QStringView mimeTag) { [[nodiscard]] QString AllowOnlyCustomEmojiProcessor(QStringView mimeTag) {
auto all = TextUtilities::SplitTags(mimeTag); auto all = TextUtilities::SplitTags(mimeTag);
for (auto i = all.begin(); i != all.end();) { for (auto i = all.begin(); i != all.end();) {
@ -78,9 +146,11 @@ namespace {
Unexpected("Action in MimeData hook."); Unexpected("Action in MimeData hook.");
} }
struct UniqueCustomEmojiContext { [[nodiscard]] std::vector<Data::ReactionId> DefaultSelected() {
base::flat_set<uint64> ids; const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d");
}; const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e");
return { Data::ReactionId{ like }, Data::ReactionId{ dislike } };
}
[[nodiscard]] bool RemoveNonCustomEmojiFragment( [[nodiscard]] bool RemoveNonCustomEmojiFragment(
not_null<QTextDocument*> document, not_null<QTextDocument*> document,
@ -100,10 +170,12 @@ struct UniqueCustomEmojiContext {
break; break;
} }
const auto id = format.property(Ui::InputField::kCustomEmojiId); const auto id = format.property(Ui::InputField::kCustomEmojiId);
if (!context.ids.emplace(id.toULongLong()).second) { const auto documentId = id.toULongLong();
if (ranges::contains(context.ids, documentId)) {
removeTill += fragment.length(); removeTill += fragment.length();
break; break;
} }
context.ids.push_back(documentId);
} }
while (removeTill == removeFrom) { while (removeTill == removeFrom) {
block = block.next(); block = block.next();
@ -131,7 +203,9 @@ bool RemoveNonCustomEmoji(
return true; return true;
} }
void SetupOnlyCustomEmojiField(not_null<Ui::InputField*> field) { void SetupOnlyCustomEmojiField(
not_null<Ui::InputField*> field,
Fn<void(std::vector<DocumentId>)> callback) {
field->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor); field->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor);
field->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook); field->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook);
@ -164,12 +238,54 @@ void SetupOnlyCustomEmojiField(not_null<Ui::InputField*> field) {
document->setPageSize(pageSize); document->setPageSize(pageSize);
} }
} }
callback(context.ids);
if (changed) { if (changed) {
field->forceProcessContentsChanges(); field->forceProcessContentsChanges();
} }
}, field->lifetime()); }, field->lifetime());
} }
[[nodiscard]] TextWithTags ComposeEmojiList(
not_null<Data::Reactions*> reactions,
const std::vector<Data::ReactionId> &list) {
auto result = TextWithTags();
const auto size = [&] {
return int(result.text.size());
};
auto added = base::flat_set<Data::ReactionId>();
const auto &all = reactions->list(Data::Reactions::Type::All);
const auto add = [&](Data::ReactionId id) {
if (!added.emplace(id).second) {
return;
}
auto unifiedId = id.custom();
const auto offset = size();
if (unifiedId) {
result.text.append('@');
} else {
result.text.append(id.emoji());
const auto i = ranges::find(all, id, &Data::Reaction::id);
if (i == end(all)) {
return;
}
unifiedId = i->selectAnimation->id;
}
const auto data = Data::SerializeCustomEmojiId(unifiedId);
const auto tag = Ui::InputField::CustomEmojiLink(data);
result.tags.append({ offset, size() - offset, tag });
};
for (const auto &id : list) {
add(id);
}
return result;
}
enum class ReactionsSelectorState {
Active,
Disabled,
Hidden,
};
struct ReactionsSelectorArgs { struct ReactionsSelectorArgs {
not_null<QWidget*> outer; not_null<QWidget*> outer;
not_null<Window::SessionController*> controller; not_null<Window::SessionController*> controller;
@ -177,13 +293,16 @@ struct ReactionsSelectorArgs {
std::vector<Data::Reaction> list; std::vector<Data::Reaction> list;
std::vector<Data::ReactionId> selected; std::vector<Data::ReactionId> selected;
Fn<void(std::vector<Data::ReactionId>)> callback; Fn<void(std::vector<Data::ReactionId>)> callback;
rpl::producer<> focusRequests; rpl::producer<ReactionsSelectorState> stateValue;
int customAllowed = 0;
bool all = false;
}; };
object_ptr<Ui::RpWidget> AddReactionsSelector( object_ptr<Ui::RpWidget> AddReactionsSelector(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
ReactionsSelectorArgs &&args) { ReactionsSelectorArgs &&args) {
using namespace ChatHelpers; using namespace ChatHelpers;
using HistoryView::Reactions::UnifiedFactoryOwner;
auto result = object_ptr<Ui::InputField>( auto result = object_ptr<Ui::InputField>(
parent, parent,
@ -191,23 +310,111 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
Ui::InputField::Mode::MultiLine, Ui::InputField::Mode::MultiLine,
std::move(args.title)); std::move(args.title));
const auto raw = result.data(); const auto raw = result.data();
const auto session = &args.controller->session();
const auto owner = &session->data();
const auto reactions = &owner->reactions();
const auto customAllowed = args.customAllowed;
struct State {
std::unique_ptr<Ui::RpWidget> overlay;
std::unique_ptr<UnifiedFactoryOwner> unifiedFactoryOwner;
UnifiedFactoryOwner::RecentFactory factory;
base::flat_set<DocumentId> allowed;
rpl::lifetime focusLifetime;
};
const auto state = raw->lifetime().make_state<State>();
state->unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(
session,
reactions->list(Data::Reactions::Type::Active));
state->factory = state->unifiedFactoryOwner->factory();
const auto customEmojiPaused = [controller = args.controller] { const auto customEmojiPaused = [controller = args.controller] {
return controller->isGifPausedAtLeastFor(PauseReason::Layer); return controller->isGifPausedAtLeastFor(PauseReason::Layer);
}; };
raw->setCustomEmojiFactory( raw->setCustomEmojiFactory([=](QStringView data, Fn<void()> update)
args.controller->session().data().customEmojiManager().factory(), -> std::unique_ptr<Ui::Text::CustomEmoji> {
std::move(customEmojiPaused)); const auto id = Data::ParseCustomEmojiData(data);
auto result = owner->customEmojiManager().create(
data,
std::move(update));
if (state->unifiedFactoryOwner->lookupReactionId(id).custom()) {
return std::make_unique<MaybeDisabledEmoji>(
std::move(result),
[=] { return state->allowed.contains(id); });
}
using namespace Ui::Text;
return std::make_unique<FirstFrameEmoji>(std::move(result));
}, std::move(customEmojiPaused));
SetupOnlyCustomEmojiField(raw); const auto callback = args.callback;
SetupOnlyCustomEmojiField(raw, [=](std::vector<DocumentId> ids) {
auto allowed = base::flat_set<DocumentId>();
auto reactions = std::vector<Data::ReactionId>();
reactions.reserve(ids.size());
allowed.reserve(std::min(customAllowed, int(ids.size())));
const auto owner = state->unifiedFactoryOwner.get();
for (const auto id : ids) {
const auto reactionId = owner->lookupReactionId(id);
if (reactionId.custom() && allowed.size() < customAllowed) {
allowed.emplace(id);
}
reactions.push_back(reactionId);
}
if (state->allowed != allowed) {
state->allowed = std::move(allowed);
raw->rawTextEdit()->update();
}
callback(std::move(reactions));
});
raw->setTextWithTags(ComposeEmojiList(reactions, args.selected));
std::move(args.focusRequests) | rpl::start_with_next([=] { using SelectorState = ReactionsSelectorState;
raw->setFocusFast(); std::move(
args.stateValue
) | rpl::start_with_next([=](SelectorState value) {
switch (value) {
case SelectorState::Active:
state->overlay = nullptr;
state->focusLifetime.destroy();
if (raw->empty()) {
raw->setTextWithTags(
ComposeEmojiList(reactions, DefaultSelected()));
}
raw->setDisabled(false);
raw->setFocusFast();
break;
case SelectorState::Disabled:
state->overlay = std::make_unique<Ui::RpWidget>(parent);
state->overlay->show();
raw->geometryValue() | rpl::start_with_next([=](QRect rect) {
state->overlay->setGeometry(rect);
}, state->overlay->lifetime());
state->overlay->paintRequest() | rpl::start_with_next([=](QRect clip) {
auto color = st::boxBg->c;
color.setAlphaF(0.5);
QPainter(state->overlay.get()).fillRect(
clip,
color);
}, state->overlay->lifetime());
[[fallthrough]];
case SelectorState::Hidden:
if (Ui::InFocusChain(raw)) {
raw->parentWidget()->setFocus();
}
raw->setDisabled(true);
raw->focusedChanges(
) | rpl::start_with_next([=](bool focused) {
if (focused) {
raw->parentWidget()->setFocus();
}
}, state->focusLifetime);
break;
}
}, raw->lifetime()); }, raw->lifetime());
const auto toggle = Ui::CreateChild<Ui::EmojiButton>( const auto toggle = Ui::CreateChild<Ui::IconButton>(
parent.get(), parent.get(),
st::boxAttachEmoji); st::manageGroupReactions);
const auto panel = Ui::CreateChild<TabbedPanel>( const auto panel = Ui::CreateChild<TabbedPanel>(
args.outer.get(), args.outer.get(),
@ -216,7 +423,11 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
nullptr, nullptr,
args.controller->uiShow(), args.controller->uiShow(),
Window::GifPauseReason::Layer, Window::GifPauseReason::Layer,
TabbedSelector::Mode::EmojiOnly)); (args.all
? TabbedSelector::Mode::FullReactions
: TabbedSelector::Mode::RecentReactions)));
panel->selector()->provideRecentEmoji(
state->unifiedFactoryOwner->unifiedIdsList());
panel->setDesiredHeightValues( panel->setDesiredHeightValues(
1., 1.,
st::emojiPanMinHeight / 2, st::emojiPanMinHeight / 2,
@ -247,7 +458,13 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
} }
return base::EventFilterResult::Continue; return base::EventFilterResult::Continue;
}; };
base::install_event_filter(args.outer, filterCallback); for (auto widget = (QWidget*)raw
; widget && widget != args.outer
; widget = widget->parentWidget()) {
base::install_event_filter(raw, widget, filterCallback);
}
base::install_event_filter(raw, args.outer, filterCallback);
scheduleUpdateEmojiPanelGeometry();
toggle->installEventFilter(panel); toggle->installEventFilter(panel);
toggle->addClickHandler([=] { toggle->addClickHandler([=] {
@ -264,33 +481,105 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
return result; return result;
} }
void AddReactionsText(
not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionNavigation*> navigation,
int allowedCustomReactions,
rpl::producer<int> customCountValue,
Fn<void(int required)> askForBoosts) {
auto ownedInner = object_ptr<Ui::VerticalLayout>(container);
const auto inner = ownedInner.data();
const auto count = inner->lifetime().make_state<rpl::variable<int>>(
std::move(customCountValue));
auto outer = container->add(
object_ptr<Ui::DividerLabel>(
container,
std::move(ownedInner),
st::defaultBoxDividerLabelPadding),
QMargins(0, st::manageGroupReactionsTextSkip, 0, 0));
const auto label = inner->add(
object_ptr<Ui::FlatLabel>(
inner,
tr::lng_manage_peer_reactions_own(
lt_link,
tr::lng_manage_peer_reactions_own_link(
) | Ui::Text::ToLink(),
Ui::Text::WithEntities),
st::boxDividerLabel));
const auto weak = base::make_weak(navigation);
label->setClickHandlerFilter([=](const auto &...) {
if (const auto strong = weak.get()) {
using Info = Window::SessionNavigation::PeerByLinkInfo;
strong->showPeerByLink(Info{
.usernameOrId = u"stickers"_q,
.resolveType = Window::ResolveType::Mention,
});
}
return false;
});
auto countString = count->value() | rpl::map([](int count) {
return TextWithEntities{ QString::number(count) };
});
auto needs = rpl::combine(
tr::lng_manage_peer_reactions_level(
lt_count,
count->value() | tr::to_count(),
lt_same_count,
std::move(countString),
Ui::Text::RichLangValue),
tr::lng_manage_peer_reactions_boost(
lt_link,
tr::lng_manage_peer_reactions_boost_link() | Ui::Text::ToLink(),
Ui::Text::RichLangValue)
) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {
a.append(' ').append(std::move(b));
return std::move(a);
});
const auto wrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
inner,
object_ptr<Ui::FlatLabel>(
inner,
std::move(needs),
st::boxDividerLabel),
QMargins{ 0, st::normalFont->height, 0, 0 }));
wrap->toggleOn(count->value() | rpl::map(
rpl::mappers::_1 > allowedCustomReactions
));
wrap->finishAnimating();
wrap->entity()->setClickHandlerFilter([=](const auto &...) {
askForBoosts(count->current());
return false;
});
}
} // namespace } // namespace
void EditAllowedReactionsBox( void EditAllowedReactionsBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation, EditAllowedReactionsArgs &&args) {
bool isGroup,
const std::vector<Data::Reaction> &list,
const Data::AllowedReactions &allowed,
Fn<void(const Data::AllowedReactions &)> callback) {
using namespace Data; using namespace Data;
using namespace rpl::mappers; using namespace rpl::mappers;
const auto iconHeight = st::editPeerReactionsPreview; const auto iconHeight = st::editPeerReactionsPreview;
box->setTitle(tr::lng_manage_peer_reactions()); box->setTitle(tr::lng_manage_peer_reactions());
box->setWidth(st::boxWideWidth);
enum class Option { enum class Option {
All, All,
Some, Some,
None, None,
}; };
using SelectorState = ReactionsSelectorState;
struct State { struct State {
base::flat_map<ReactionId, not_null<Ui::SettingsButton*>> toggles;
rpl::variable<Option> option; // For groups. rpl::variable<Option> option; // For groups.
rpl::variable<bool> anyToggled; // For channels. rpl::variable<SelectorState> selectorState;
rpl::event_stream<bool> forceToggleAll; // For channels. std::vector<Data::ReactionId> selected;
rpl::event_stream<> focusRequests; rpl::variable<int> customCount;
}; };
const auto allowed = args.allowed;
const auto optionInitial = (allowed.type != AllowedReactionsType::Some) const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
? Option::All ? Option::All
: allowed.some.empty() : allowed.some.empty()
@ -298,58 +587,24 @@ void EditAllowedReactionsBox(
: Option::Some; : Option::Some;
const auto state = box->lifetime().make_state<State>(State{ const auto state = box->lifetime().make_state<State>(State{
.option = optionInitial, .option = optionInitial,
.anyToggled = (optionInitial != Option::None),
}); });
const auto collect = [=] {
auto result = AllowedReactions();
if (!isGroup || state->option.current() == Option::Some) {
result.some.reserve(state->toggles.size());
for (const auto &[id, button] : state->toggles) {
if (button->toggled()) {
result.some.push_back(id);
}
}
}
result.type = isGroup
? (state->option.current() != Option::All
? AllowedReactionsType::Some
: AllowedReactionsType::All)
: (result.some.size() == state->toggles.size())
? AllowedReactionsType::Default
: AllowedReactionsType::Some;
return result;
};
const auto container = box->verticalLayout(); const auto container = box->verticalLayout();
const auto isGroup = args.isGroup;
const auto enabled = isGroup const auto enabled = isGroup
? nullptr ? nullptr
: container->add(object_ptr<Ui::SettingsButton>( : container->add(object_ptr<Ui::SettingsButton>(
container, container.get(),
tr::lng_manage_peer_reactions_enable(), tr::lng_manage_peer_reactions_enable(),
st::manageGroupButton.button)); st::manageGroupNoIconButton.button));
if (enabled && !list.empty()) {
AddReactionAnimatedIcon(
enabled,
enabled->sizeValue(
) | rpl::map([=](const QSize &size) {
return QPoint(
st::manageGroupButton.iconPosition.x(),
(size.height() - iconHeight) / 2);
}),
iconHeight,
list.front(),
rpl::never<>(),
rpl::never<>(),
&enabled->lifetime());
}
if (enabled) { if (enabled) {
enabled->toggleOn(state->anyToggled.value()); enabled->toggleOn(rpl::single(optionInitial != Option::None));
enabled->toggledChanges( enabled->toggledValue(
) | rpl::filter([=](bool value) { ) | rpl::start_with_next([=](bool value) {
return (value != state->anyToggled.current()); state->selectorState = value
}) | rpl::start_to_stream(state->forceToggleAll, enabled->lifetime()); ? SelectorState::Active
: SelectorState::Disabled;
}, enabled->lifetime());
} }
const auto group = std::make_shared<Ui::RadioenumGroup<Option>>( const auto group = std::make_shared<Ui::RadioenumGroup<Option>>(
state->option.current()); state->option.current());
@ -395,44 +650,100 @@ void EditAllowedReactionsBox(
container, container,
object_ptr<Ui::VerticalLayout>(container))); object_ptr<Ui::VerticalLayout>(container)));
if (wrap) { if (wrap) {
wrap->toggleOn(state->option.value() | rpl::map(_1 == Option::Some)); wrap->toggleOn(state->option.value(
) | rpl::map(_1 == Option::Some) | rpl::before_next([=](bool some) {
if (!some) {
state->selectorState = SelectorState::Hidden;
}
}) | rpl::after_next([=](bool some) {
if (some) {
state->selectorState = SelectorState::Active;
}
}));
wrap->finishAnimating(); wrap->finishAnimating();
} }
const auto reactions = wrap ? wrap->entity() : container.get(); const auto reactions = wrap ? wrap->entity() : container.get();
Ui::AddSkip(reactions); Ui::AddSkip(reactions);
const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d"); const auto all = args.list;
const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e"); auto selected = (allowed.type != AllowedReactionsType::Some)
auto selected = allowed.some; ? (all
if (selected.empty()) { | ranges::views::transform(&Data::Reaction::id)
selected.push_back(Data::ReactionId(like)); | ranges::to_vector)
selected.push_back(Data::ReactionId(dislike)); : allowed.some;
}
const auto changed = [=](std::vector<Data::ReactionId> chosen) { const auto changed = [=](std::vector<Data::ReactionId> chosen) {
state->selected = std::move(chosen);
state->customCount = ranges::count_if(
state->selected,
&Data::ReactionId::custom);
}; };
changed(selected.empty() ? DefaultSelected() : std::move(selected));
reactions->add(AddReactionsSelector(reactions, { reactions->add(AddReactionsSelector(reactions, {
.outer = box->getDelegate()->outerContainer(), .outer = box->getDelegate()->outerContainer(),
.controller = navigation->parentController(), .controller = args.navigation->parentController(),
.title = (enabled .title = (enabled
? tr::lng_manage_peer_reactions_available() ? tr::lng_manage_peer_reactions_available()
: tr::lng_manage_peer_reactions_some_title()), : tr::lng_manage_peer_reactions_some_title()),
.list = list, .list = all,
.selected = std::move(selected), .selected = state->selected,
.callback = changed, .callback = changed,
.focusRequests = state->focusRequests.events(), .stateValue = state->selectorState.value(),
.customAllowed = args.allowedCustomReactions,
.all = !args.isGroup,
}), st::boxRowPadding); }), st::boxRowPadding);
box->setFocusCallback([=] { box->setFocusCallback([=] {
if (!wrap || state->option.current() == Option::Some) { if (!wrap || state->option.current() == Option::Some) {
state->focusRequests.fire({}); state->selectorState.force_assign(SelectorState::Active);
} }
}); });
if (!isGroup) {
AddReactionsText(
container,
args.navigation,
args.allowedCustomReactions,
state->customCount.value(),
args.askForBoosts);
}
const auto total = int(all.size());
const auto collect = [=] {
auto result = AllowedReactions();
if (isGroup
? (state->option.current() == Option::Some)
: (enabled->toggled())) {
result.some = state->selected;
}
auto some = result.some;
auto simple = all | ranges::views::transform(
&Data::Reaction::id
) | ranges::to_vector;
ranges::sort(some);
ranges::sort(simple);
result.type = isGroup
? (state->option.current() != Option::All
? AllowedReactionsType::Some
: AllowedReactionsType::All)
: (some == simple)
? AllowedReactionsType::Default
: AllowedReactionsType::Some;
return result;
};
box->addButton(tr::lng_settings_save(), [=] { box->addButton(tr::lng_settings_save(), [=] {
const auto result = collect(); const auto result = collect();
if (!isGroup) {
const auto custom = ranges::count_if(
result.some,
&Data::ReactionId::custom);
if (custom > args.allowedCustomReactions) {
args.askForBoosts(custom);
return;
}
}
box->closeBox(); box->closeBox();
callback(result); args.save(result);
}); });
box->addButton(tr::lng_cancel(), [=] { box->addButton(tr::lng_cancel(), [=] {
box->closeBox(); box->closeBox();

View file

@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
class PeerData; #include "data/data_peer.h"
namespace Data { namespace Data {
struct Reaction; struct Reaction;
@ -22,13 +22,19 @@ namespace Window {
class SessionNavigation; class SessionNavigation;
} // namespace Window } // namespace Window
struct EditAllowedReactionsArgs {
not_null<Window::SessionNavigation*> navigation;
int allowedCustomReactions = 0;
bool isGroup = false;
std::vector<Data::Reaction> list;
Data::AllowedReactions allowed;
Fn<void(int required)> askForBoosts;
Fn<void(const Data::AllowedReactions &)> save;
};
void EditAllowedReactionsBox( void EditAllowedReactionsBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation, EditAllowedReactionsArgs &&args);
bool isGroup,
const std::vector<Data::Reaction> &list,
const Data::AllowedReactions &allowed,
Fn<void(const Data::AllowedReactions &)> callback);
void SaveAllowedReactions( void SaveAllowedReactions(
not_null<PeerData*> peer, not_null<PeerData*> peer,

View file

@ -785,6 +785,10 @@ void EmojiListWidget::unloadCustomIn(const SectionInfo &info) {
object_ptr<TabbedSelector::InnerFooter> EmojiListWidget::createFooter() { object_ptr<TabbedSelector::InnerFooter> EmojiListWidget::createFooter() {
Expects(_footer == nullptr); Expects(_footer == nullptr);
if (_mode == EmojiListMode::RecentReactions) {
return { nullptr };
}
using FooterDescriptor = StickersListFooter::Descriptor; using FooterDescriptor = StickersListFooter::Descriptor;
const auto flag = powerSavingFlag(); const auto flag = powerSavingFlag();
const auto footerPaused = [method = pausedMethod(), flag]() { const auto footerPaused = [method = pausedMethod(), flag]() {

View file

@ -331,8 +331,12 @@ TabbedSelector::TabbedSelector(
Mode mode) Mode mode)
: TabbedSelector(parent, { : TabbedSelector(parent, {
.show = std::move(show), .show = std::move(show),
.st = ((mode == Mode::EmojiStatus || mode == Mode::BackgroundEmoji) .st = ((mode == Mode::EmojiStatus
|| mode == Mode::BackgroundEmoji
|| mode == Mode::FullReactions)
? st::statusEmojiPan ? st::statusEmojiPan
: (mode == Mode::RecentReactions)
? st::backgroundEmojiPan
: st::defaultEmojiPan), : st::defaultEmojiPan),
.level = level, .level = level,
.mode = mode, .mode = mode,
@ -385,7 +389,11 @@ TabbedSelector::TabbedSelector(
resize(st::emojiPanWidth, st::emojiPanMaxHeight); resize(st::emojiPanWidth, st::emojiPanMaxHeight);
for (auto &tab : _tabs) { for (auto &tab : _tabs) {
tab.footer()->hide(); if (tab.hasFooter()) {
tab.footer()->hide();
} else {
_noFooter = true;
}
tab.widget()->hide(); tab.widget()->hide();
} }
if (tabbed()) { if (tabbed()) {
@ -515,6 +523,10 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) {
? EmojiMode::EmojiStatus ? EmojiMode::EmojiStatus
: _mode == Mode::BackgroundEmoji : _mode == Mode::BackgroundEmoji
? EmojiMode::BackgroundEmoji ? EmojiMode::BackgroundEmoji
: _mode == Mode::FullReactions
? EmojiMode::FullReactions
: _mode == Mode::RecentReactions
? EmojiMode::RecentReactions
: EmojiMode::Full), : EmojiMode::Full),
.customTextColor = _customTextColor, .customTextColor = _customTextColor,
.paused = paused, .paused = paused,
@ -694,10 +706,16 @@ void TabbedSelector::updateScrollGeometry(QSize oldSize) {
} }
void TabbedSelector::updateFooterGeometry() { void TabbedSelector::updateFooterGeometry() {
_footerTop = _dropDown ? 0 : (height() - _st.footer); _footerTop = _dropDown
? 0
: _noFooter
? (height() - _roundRadius)
: (height() - _st.footer);
for (auto &tab : _tabs) { for (auto &tab : _tabs) {
tab.footer()->resizeToWidth(width()); if (tab.hasFooter()) {
tab.footer()->moveToLeft(0, _footerTop); tab.footer()->resizeToWidth(width());
tab.footer()->moveToLeft(0, _footerTop);
}
} }
} }
@ -767,7 +785,7 @@ void TabbedSelector::paintContent(QPainter &p) {
0, 0,
_footerTop, _footerTop,
width(), width(),
_st.footer); _noFooter ? _roundRadius : _st.footer);
Ui::FillRoundRect(p, footerPart, footerBg, { Ui::FillRoundRect(p, footerPart, footerBg, {
.p = { .p = {
_dropDown ? pixmaps.p[0] : QPixmap(), _dropDown ? pixmaps.p[0] : QPixmap(),
@ -802,7 +820,7 @@ void TabbedSelector::paintContent(QPainter &p) {
} }
int TabbedSelector::marginTop() const { int TabbedSelector::marginTop() const {
return _dropDown return (_dropDown && !_noFooter)
? _st.footer ? _st.footer
: _tabsSlider : _tabsSlider
? (_tabsSlider->height() - st::lineWidth) ? (_tabsSlider->height() - st::lineWidth)
@ -810,15 +828,19 @@ int TabbedSelector::marginTop() const {
} }
int TabbedSelector::scrollTop() const { int TabbedSelector::scrollTop() const {
return tabbed() ? marginTop() : _dropDown ? _st.footer : 0; return tabbed()
? marginTop()
: (_dropDown && !_noFooter)
? _st.footer
: 0;
} }
int TabbedSelector::marginBottom() const { int TabbedSelector::marginBottom() const {
return _dropDown ? _roundRadius : _st.footer; return (_dropDown || _noFooter) ? _roundRadius : _st.footer;
} }
int TabbedSelector::scrollBottom() const { int TabbedSelector::scrollBottom() const {
return _dropDown ? 0 : marginBottom(); return (_dropDown || _noFooter) ? 0 : marginBottom();
} }
void TabbedSelector::refreshStickers() { void TabbedSelector::refreshStickers() {
@ -1013,7 +1035,9 @@ void TabbedSelector::showAll() {
if (isRestrictedView()) { if (isRestrictedView()) {
_restrictedLabel->show(); _restrictedLabel->show();
} else { } else {
currentTab()->footer()->show(); if (currentTab()->hasFooter()) {
currentTab()->footer()->show();
}
_scroll->show(); _scroll->show();
_bottomShadow->setVisible(_mode == Mode::EmojiStatus); _bottomShadow->setVisible(_mode == Mode::EmojiStatus);
} }
@ -1100,7 +1124,7 @@ void TabbedSelector::fillTabsSliderSections() {
} }
bool TabbedSelector::hasSectionIcons() const { bool TabbedSelector::hasSectionIcons() const {
return !_restrictedLabel; return !_restrictedLabel && !_noFooter;
} }
void TabbedSelector::switchTab() { void TabbedSelector::switchTab() {
@ -1125,7 +1149,9 @@ void TabbedSelector::switchTab() {
auto widget = _scroll->takeWidget<Inner>(); auto widget = _scroll->takeWidget<Inner>();
widget->setParent(this); widget->setParent(this);
widget->hide(); widget->hide();
currentTab()->footer()->hide(); if (currentTab()->hasFooter()) {
currentTab()->footer()->hide();
}
currentTab()->returnWidget(std::move(widget)); currentTab()->returnWidget(std::move(widget));
_currentTabType = newTabType; _currentTabType = newTabType;

View file

@ -82,6 +82,8 @@ enum class TabbedSelectorMode {
MediaEditor, MediaEditor,
EmojiStatus, EmojiStatus,
BackgroundEmoji, BackgroundEmoji,
FullReactions,
RecentReactions,
}; };
struct TabbedSelectorDescriptor { struct TabbedSelectorDescriptor {
@ -192,16 +194,19 @@ private:
object_ptr<Inner> takeWidget(); object_ptr<Inner> takeWidget();
void returnWidget(object_ptr<Inner> widget); void returnWidget(object_ptr<Inner> widget);
SelectorTab type() const { [[nodiscard]] SelectorTab type() const {
return _type; return _type;
} }
int index() const { [[nodiscard]] int index() const {
return _index; return _index;
} }
Inner *widget() const { [[nodiscard]] Inner *widget() const {
return _weak; return _weak;
} }
not_null<InnerFooter*> footer() const { [[nodiscard]] bool hasFooter() const {
return _footer != nullptr;
}
[[nodiscard]] not_null<InnerFooter*> footer() const {
return _footer; return _footer;
} }
@ -209,7 +214,7 @@ private:
void saveScrollTop(int scrollTop) { void saveScrollTop(int scrollTop) {
_scrollTop = scrollTop; _scrollTop = scrollTop;
} }
int getScrollTop() const { [[nodiscard]] int getScrollTop() const {
return _scrollTop; return _scrollTop;
} }
@ -279,6 +284,7 @@ private:
Mode _mode = Mode::Full; Mode _mode = Mode::Full;
int _roundRadius = 0; int _roundRadius = 0;
int _footerTop = 0; int _footerTop = 0;
bool _noFooter = false;
Ui::CornersPixmaps _panelRounding; Ui::CornersPixmaps _panelRounding;
Ui::CornersPixmaps _categoriesRounding; Ui::CornersPixmaps _categoriesRounding;
PeerData *_currentPeer = nullptr; PeerData *_currentPeer = nullptr;

View file

@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "base/qt/qt_compare.h"
namespace Data { namespace Data {
struct ReactionId { struct ReactionId {
@ -24,6 +26,13 @@ struct ReactionId {
const auto custom = std::get_if<DocumentId>(&data); const auto custom = std::get_if<DocumentId>(&data);
return custom ? *custom : DocumentId(); return custom ? *custom : DocumentId();
} }
friend inline auto operator<=>(
const ReactionId &,
const ReactionId &) = default;
friend inline bool operator==(
const ReactionId &a,
const ReactionId &b) = default;
}; };
struct MessageReaction { struct MessageReaction {
@ -32,13 +41,6 @@ struct MessageReaction {
bool my = false; bool my = false;
}; };
inline bool operator<(const ReactionId &a, const ReactionId &b) {
return a.data < b.data;
}
inline bool operator==(const ReactionId &a, const ReactionId &b) {
return a.data == b.data;
}
[[nodiscard]] QString ReactionEntityData(const ReactionId &id); [[nodiscard]] QString ReactionEntityData(const ReactionId &id);
[[nodiscard]] ReactionId ReactionFromMTP(const MTPReaction &reaction); [[nodiscard]] ReactionId ReactionFromMTP(const MTPReaction &reaction);

View file

@ -109,6 +109,87 @@ bool StripEmoji::readyInDefaultState() {
} // namespace } // namespace
UnifiedFactoryOwner::UnifiedFactoryOwner(
not_null<Main::Session*> session,
const std::vector<Data::Reaction> &reactions,
Strip *strip)
: _session(session)
, _strip(strip) {
auto index = 0;
const auto inStrip = _strip ? _strip->count() : 0;
_unifiedIdsList.reserve(reactions.size());
for (const auto &reaction : reactions) {
if (const auto id = reaction.id.custom()) {
_unifiedIdsList.push_back(id);
} else {
_unifiedIdsList.push_back(reaction.selectAnimation->id);
}
const auto unifiedId = _unifiedIdsList.back();
if (!reaction.id.custom()) {
_defaultReactionIds.emplace(unifiedId, reaction.id.emoji());
}
if (index + 1 < inStrip) {
_defaultReactionInStripMap.emplace(unifiedId, index++);
}
}
_stripPaintOneShift = [&] {
// See EmojiListWidget custom emoji position resolving.
const auto size = st::reactStripSize;
const auto area = st::emojiPanArea;
const auto areaPosition = QPoint(
(size - area.width()) / 2,
(size - area.height()) / 2);
const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio();
const auto innerPosition = QPoint(
(area.width() - esize) / 2,
(area.height() - esize) / 2);
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
const auto customSkip = (esize - customSize) / 2;
const auto customPosition = QPoint(customSkip, customSkip);
return areaPosition + innerPosition + customPosition;
}();
_defaultReactionShift = QPoint(
(st::reactStripSize - st::reactStripImage) / 2,
(st::reactStripSize - st::reactStripImage) / 2
) - _stripPaintOneShift;
}
Data::ReactionId UnifiedFactoryOwner::lookupReactionId(
DocumentId unifiedId) const {
const auto i = _defaultReactionIds.find(unifiedId);
return (i != end(_defaultReactionIds))
? Data::ReactionId{ i->second }
: Data::ReactionId{ unifiedId };
}
UnifiedFactoryOwner::RecentFactory UnifiedFactoryOwner::factory() {
return [=](DocumentId id, Fn<void()> repaint)
-> std::unique_ptr<Ui::Text::CustomEmoji> {
const auto tag = Data::CustomEmojiManager::SizeTag::Large;
const auto sizeOverride = st::reactStripImage;
const auto isDefaultReaction = _defaultReactionIds.contains(id);
const auto manager = &_session->data().customEmojiManager();
auto result = isDefaultReaction
? std::make_unique<Ui::Text::ShiftedEmoji>(
manager->create(id, std::move(repaint), tag, sizeOverride),
_defaultReactionShift)
: manager->create(id, std::move(repaint), tag);
const auto i = _defaultReactionInStripMap.find(id);
if (i != end(_defaultReactionInStripMap)) {
Assert(_strip != nullptr);
return std::make_unique<StripEmoji>(
std::move(result),
_strip,
-_stripPaintOneShift,
i->second);
}
return result;
};
}
Selector::Selector( Selector::Selector(
not_null<QWidget*> parent, not_null<QWidget*> parent,
const style::EmojiPan &st, const style::EmojiPan &st,
@ -749,65 +830,10 @@ void Selector::cacheExpandIcon() {
void Selector::createList() { void Selector::createList() {
using namespace ChatHelpers; using namespace ChatHelpers;
auto recent = _recent; _unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(
auto defaultReactionIds = base::flat_map<DocumentId, QString>(); &_show->session(),
if (_strip) { _strip ? _reactions.recent : std::vector<Data::Reaction>(),
recent.reserve(recentCount()); _strip.get());
auto index = 0;
const auto inStrip = _strip->count();
for (const auto &reaction : _reactions.recent) {
if (const auto id = reaction.id.custom()) {
recent.push_back(id);
} else {
recent.push_back(reaction.selectAnimation->id);
defaultReactionIds.emplace(recent.back(), reaction.id.emoji());
}
if (index + 1 < inStrip) {
_defaultReactionInStripMap.emplace(recent.back(), index++);
}
};
}
const auto manager = &_show->session().data().customEmojiManager();
_stripPaintOneShift = [&] {
// See EmojiListWidget custom emoji position resolving.
const auto area = st::emojiPanArea;
const auto areaPosition = QPoint(
(_size - area.width()) / 2,
(_size - area.height()) / 2);
const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio();
const auto innerPosition = QPoint(
(area.width() - esize) / 2,
(area.height() - esize) / 2);
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
const auto customSkip = (esize - customSize) / 2;
const auto customPosition = QPoint(customSkip, customSkip);
return areaPosition + innerPosition + customPosition;
}();
_defaultReactionShift = QPoint(
(_size - st::reactStripImage) / 2,
(_size - st::reactStripImage) / 2
) - _stripPaintOneShift;
auto factory = [=](DocumentId id, Fn<void()> repaint)
-> std::unique_ptr<Ui::Text::CustomEmoji> {
const auto tag = Data::CustomEmojiManager::SizeTag::Large;
const auto sizeOverride = st::reactStripImage;
const auto isDefaultReaction = defaultReactionIds.contains(id);
auto result = isDefaultReaction
? std::make_unique<Ui::Text::ShiftedEmoji>(
manager->create(id, std::move(repaint), tag, sizeOverride),
_defaultReactionShift)
: manager->create(id, std::move(repaint), tag);
const auto i = _defaultReactionInStripMap.find(id);
if (i != end(_defaultReactionInStripMap)) {
Assert(_strip != nullptr);
return std::make_unique<StripEmoji>(
std::move(result),
_strip.get(),
-_stripPaintOneShift,
i->second);
}
return result;
};
_scroll = Ui::CreateChild<Ui::ScrollArea>(this, st::reactPanelScroll); _scroll = Ui::CreateChild<Ui::ScrollArea>(this, st::reactPanelScroll);
_scroll->hide(); _scroll->hide();
@ -821,8 +847,10 @@ void Selector::createList() {
.show = _show, .show = _show,
.mode = _listMode, .mode = _listMode,
.paused = [] { return false; }, .paused = [] { return false; },
.customRecentList = std::move(recent), .customRecentList = (_strip
.customRecentFactory = std::move(factory), ? _unifiedFactoryOwner->unifiedIdsList()
: _recent),
.customRecentFactory = _unifiedFactoryOwner->factory(),
.st = st, .st = st,
}) })
).data(); ).data();
@ -831,13 +859,8 @@ void Selector::createList() {
_list->customChosen( _list->customChosen(
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
const auto id = DocumentId{ data.document->id };
const auto i = defaultReactionIds.find(id);
const auto reactionId = (i != end(defaultReactionIds))
? Data::ReactionId{ i->second }
: Data::ReactionId{ id };
_chosen.fire({ _chosen.fire({
.id = reactionId, .id = _unifiedFactoryOwner->lookupReactionId(data.document->id),
.icon = data.messageSendingFrom.frame, .icon = data.messageSendingFrom.frame,
.globalGeometry = data.messageSendingFrom.globalStartGeometry, .globalGeometry = data.messageSendingFrom.globalStartGeometry,
}); });

View file

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
namespace Data { namespace Data {
struct Reaction;
struct ReactionId; struct ReactionId;
} // namespace Data } // namespace Data
@ -39,6 +40,39 @@ class PlainShadow;
namespace HistoryView::Reactions { namespace HistoryView::Reactions {
class UnifiedFactoryOwner final {
public:
using RecentFactory = Fn<std::unique_ptr<Ui::Text::CustomEmoji>(
DocumentId,
Fn<void()>)>;
UnifiedFactoryOwner(
not_null<Main::Session*> session,
const std::vector<Data::Reaction> &reactions,
Strip *strip = nullptr);
[[nodiscard]] const std::vector<DocumentId> &unifiedIdsList() const {
return _unifiedIdsList;
}
[[nodiscard]] Data::ReactionId lookupReactionId(
DocumentId unifiedId) const;
[[nodiscard]] RecentFactory factory();
private:
const not_null<Main::Session*> _session;
Strip *_strip = nullptr;
std::vector<DocumentId> _unifiedIdsList;
base::flat_map<DocumentId, QString> _defaultReactionIds;
base::flat_map<DocumentId, int> _defaultReactionInStripMap;
QPoint _defaultReactionShift;
QPoint _stripPaintOneShift;
};
class Selector final : public Ui::RpWidget { class Selector final : public Ui::RpWidget {
public: public:
Selector( Selector(
@ -146,10 +180,7 @@ private:
const std::vector<DocumentId> _recent; const std::vector<DocumentId> _recent;
const ChatHelpers::EmojiListMode _listMode; const ChatHelpers::EmojiListMode _listMode;
Fn<void()> _jumpedToPremium; Fn<void()> _jumpedToPremium;
base::flat_map<DocumentId, int> _defaultReactionInStripMap;
Ui::RoundAreaWithShadow _cachedRound; Ui::RoundAreaWithShadow _cachedRound;
QPoint _defaultReactionShift;
QPoint _stripPaintOneShift;
std::unique_ptr<Strip> _strip; std::unique_ptr<Strip> _strip;
rpl::event_stream<ChosenReaction> _chosen; rpl::event_stream<ChosenReaction> _chosen;
@ -160,6 +191,7 @@ private:
Ui::ScrollArea *_scroll = nullptr; Ui::ScrollArea *_scroll = nullptr;
ChatHelpers::EmojiListWidget *_list = nullptr; ChatHelpers::EmojiListWidget *_list = nullptr;
ChatHelpers::StickersListFooter *_footer = nullptr; ChatHelpers::StickersListFooter *_footer = nullptr;
std::unique_ptr<UnifiedFactoryOwner> _unifiedFactoryOwner;
Ui::PlainShadow *_shadow = nullptr; Ui::PlainShadow *_shadow = nullptr;
rpl::variable<int> _shadowTop = 0; rpl::variable<int> _shadowTop = 0;
rpl::variable<int> _shadowSkip = 0; rpl::variable<int> _shadowSkip = 0;
@ -175,7 +207,7 @@ private:
QMargins _padding; QMargins _padding;
int _specialExpandTopSkip = 0; int _specialExpandTopSkip = 0;
int _collapsedTopSkip = 0; int _collapsedTopSkip = 0;
int _size = 0; const int _size = 0;
int _recentRows = 0; int _recentRows = 0;
int _columns = 0; int _columns = 0;
int _skipx = 0; int _skipx = 0;

View file

@ -601,10 +601,19 @@ manageDeleteGroupButton: SettingsCountButton(manageGroupNoIconButton) {
} }
} }
manageGroupReactions: IconButton(defaultIconButton) {
width: 24px;
height: 36px;
icon: icon{{ "info/edit/stickers_add", historyComposeIconFg }};
iconOver: icon{{ "info/edit/stickers_add", historyComposeIconFgOver }};
}
manageGroupReactionsField: InputField(defaultInputField) { manageGroupReactionsField: InputField(defaultInputField) {
textMargins: margins(1px, 26px, 31px, 4px); textMargins: margins(1px, 38px, 24px, 8px);
placeholderShift: -32px;
heightMin: 66px;
heightMax: 158px; heightMax: 158px;
} }
manageGroupReactionsTextSkip: 16px;
infoEmptyFg: windowSubTextFg; infoEmptyFg: windowSubTextFg;
infoEmptyPhoto: icon {{ "info/info_media_photo_empty", infoEmptyFg }}; infoEmptyPhoto: icon {{ "info/info_media_photo_empty", infoEmptyFg }};

View file

@ -467,12 +467,25 @@ void AskBoostBox(
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
auto title = tr::lng_boost_channel_title_color(); auto title = v::is<AskBoostChannelColor>(data.reason.data)
auto text = rpl::combine( ? tr::lng_boost_channel_title_color()
tr::lng_boost_channel_needs_level_color( : tr::lng_boost_channel_title_reactions();
auto reasonText = v::match(data.reason.data, [&](
AskBoostChannelColor data) {
return tr::lng_boost_channel_needs_level_color(
lt_count, lt_count,
rpl::single(float64(data.requiredLevel)), rpl::single(float64(data.requiredLevel)),
Ui::Text::RichLangValue), Ui::Text::RichLangValue);
}, [&](AskBoostCustomReactions data) {
return tr::lng_boost_channel_needs_level_reactions(
lt_count,
rpl::single(float64(data.count)),
lt_same_count,
rpl::single(TextWithEntities{ QString::number(data.count) }),
Ui::Text::RichLangValue);
});
auto text = rpl::combine(
std::move(reasonText),
tr::lng_boost_channel_ask(Ui::Text::RichLangValue) tr::lng_boost_channel_ask(Ui::Text::RichLangValue)
) | rpl::map([](TextWithEntities &&text, TextWithEntities &&ask) { ) | rpl::map([](TextWithEntities &&text, TextWithEntities &&ask) {
return text.append(u"\n\n"_q).append(std::move(ask)); return text.append(u"\n\n"_q).append(std::move(ask));

View file

@ -50,10 +50,24 @@ void GiftForBoostsBox(
void GiftedNoBoostsBox(not_null<GenericBox*> box); void GiftedNoBoostsBox(not_null<GenericBox*> box);
void PremiumForBoostsBox(not_null<GenericBox*> box, Fn<void()> buyPremium); void PremiumForBoostsBox(not_null<GenericBox*> box, Fn<void()> buyPremium);
struct AskBoostChannelColor {
int requiredLevel = 0;
};
struct AskBoostCustomReactions {
int count = 0;
};
struct AskBoostReason {
std::variant<
AskBoostChannelColor,
AskBoostCustomReactions> data;
};
struct AskBoostBoxData { struct AskBoostBoxData {
QString link; QString link;
BoostCounters boost; BoostCounters boost;
int requiredLevel = 0; AskBoostReason reason;
}; };
void AskBoostBox( void AskBoostBox(