Implement improved allowed reactions editing.

This commit is contained in:
John Preston 2022-08-30 13:21:58 +04:00
parent 021e275336
commit 5e81c65ea6
9 changed files with 291 additions and 163 deletions

View file

@ -1195,13 +1195,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_permissions" = "Permissions";
"lng_manage_peer_invite_links" = "Invite links";
"lng_manage_peer_reactions" = "Reactions";
"lng_manage_peer_reactions_on" = "All";
"lng_manage_peer_reactions_off" = "Off";
"lng_manage_peer_requests" = "Member Requests";
"lng_manage_peer_requests_channel" = "Subscriber Requests";
"lng_manage_peer_reactions_enable" = "Enable Reactions";
"lng_manage_peer_reactions_about" = "Allow members to react to group messages.";
"lng_manage_peer_reactions_about_channel" = "Allow subscribers to react to channel posts.";
"lng_manage_peer_reactions_all" = "All reactions";
"lng_manage_peer_reactions_all_about" = "Members of the group can use any emoji as reactions to messages.";
"lng_manage_peer_reactions_all_about_channel" = "Subscribers of this channel can use any emoji as reactions to messages.";
"lng_manage_peer_reactions_some" = "Some reactions";
"lng_manage_peer_reactions_some_about" = "You can select emoji that will allow members of your group to react to messages.";
"lng_manage_peer_reactions_some_about_channel" = "You can select emoji that will allow subscribers of this channel to react to messages.";
"lng_manage_peer_reactions_none" = "No reactions";
"lng_manage_peer_reactions_none_about" = "Members of the group can't add any reactions to messages.";
"lng_manage_peer_reactions_none_about_channel" = "Subscribers of this channel can't add any reactions to messages.";
"lng_manage_peer_reactions_some_title" = "Only allow these reactions";
"lng_manage_peer_reactions_available" = "Available reactions";
"lng_manage_peer_group_type" = "Group type";

View file

@ -988,22 +988,31 @@ void Controller::fillManageSection() {
if (canEditReactions()) {
const auto session = &_peer->session();
auto reactionsCount = Info::Profile::MigratedOrMeValue(
auto allowedReactions = Info::Profile::MigratedOrMeValue(
_peer
) | rpl::map(
Info::Profile::AllowedReactionsCountValue
) | rpl::flatten_latest();
auto fullCount = Info::Profile::FullReactionsCountValue(session);
) | rpl::map([=](not_null<PeerData*> peer) {
return peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::Reactions
) | rpl::map([=] {
return Data::PeerAllowedReactions(peer);
});
}) | rpl::flatten_latest();
auto label = rpl::combine(
std::move(reactionsCount),
std::move(fullCount)
) | rpl::map([=](int allowed, int total) {
return allowed
? QString::number(allowed) + " / " + QString::number(total)
std::move(allowedReactions),
Info::Profile::FullReactionsCountValue(session)
) | rpl::map([=](const Data::AllowedReactions &allowed, int total) {
const auto some = int(allowed.some.size());
return (allowed.type != Data::AllowedReactionsType::Some)
? tr::lng_manage_peer_reactions_on(tr::now)
: some
? (QString::number(some)
+ " / "
+ QString::number(std::max(some, total)))
: tr::lng_manage_peer_reactions_off(tr::now);
});
const auto done = [=](const std::vector<QString> &chosen, bool all) {
SaveAllowedReactions(_peer, chosen, all);
const auto done = [=](const Data::AllowedReactions &chosen) {
SaveAllowedReactions(_peer, chosen);
};
AddButtonWithCount(
_controls.buttonsLayout,
@ -1012,6 +1021,7 @@ void Controller::fillManageSection() {
[=] {
_navigation->parentController()->show(Box(
EditAllowedReactionsBox,
_navigation,
!_peer->isBroadcast(),
session->data().reactions().list(
Data::Reactions::Type::Active),

View file

@ -18,34 +18,57 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
#include "settings/settings_common.h"
#include "window/window_session_controller.h"
#include "styles/style_settings.h"
#include "styles/style_info.h"
void EditAllowedReactionsBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
bool isGroup,
const std::vector<Data::Reaction> &list,
const Data::AllowedReactions &allowed,
Fn<void(const std::vector<QString> &, bool all)> callback) {
Fn<void(const Data::AllowedReactions &)> callback) {
using namespace Data;
using namespace rpl::mappers;
const auto iconHeight = st::editPeerReactionsPreview;
box->setTitle(tr::lng_manage_peer_reactions());
enum class Option {
All,
Some,
None,
};
struct State {
base::flat_map<QString, not_null<Ui::SettingsButton*>> toggles;
rpl::variable<bool> anyToggled;
rpl::event_stream<bool> forceToggleAll;
base::flat_map<ReactionId, not_null<Ui::SettingsButton*>> toggles;
rpl::variable<Option> option;
};
const auto state = box->lifetime().make_state<State>(State{
.anyToggled = (allowed.type != Data::AllowedReactionsType::Some),
.option = (allowed.type != AllowedReactionsType::Some
? Option::All
: allowed.some.empty()
? Option::None
: Option::Some),
});
const auto collect = [=] {
auto result = std::vector<QString>();
result.reserve(state->toggles.size());
for (const auto &[emoji, button] : state->toggles) {
if (button->toggled()) {
result.push_back(emoji);
auto result = AllowedReactions{
.type = (state->option.current() != Option::All
? AllowedReactionsType::Some
: isGroup
? AllowedReactionsType::All
: AllowedReactionsType::Default),
};
if (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);
}
}
}
return result;
@ -53,52 +76,64 @@ void EditAllowedReactionsBox(
const auto container = box->verticalLayout();
const auto enabled = Settings::AddButton(
container,
tr::lng_manage_peer_reactions_enable(),
st::manageGroupButton.button);
if (!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());
}
enabled->toggleOn(state->anyToggled.value());
enabled->toggledChanges(
) | rpl::filter([=](bool value) {
return (value != state->anyToggled.current());
}) | rpl::start_to_stream(state->forceToggleAll, enabled->lifetime());
const auto group = std::make_shared<Ui::RadioenumGroup<Option>>(
state->option.current());
group->setChangedCallback([=](Option value) {
state->option = value;
});
const auto addOption = [&](Option option, const QString &text) {
container->add(
object_ptr<Ui::Radioenum<Option>>(
container,
group,
option,
text,
st::settingsSendType),
st::settingsSendTypePadding);
};
addOption(Option::All, tr::lng_manage_peer_reactions_all(tr::now));
addOption(Option::Some, tr::lng_manage_peer_reactions_some(tr::now));
addOption(Option::None, tr::lng_manage_peer_reactions_none(tr::now));
const auto about = [isGroup](Option option) {
switch (option) {
case Option::All: return isGroup
? tr::lng_manage_peer_reactions_all_about()
: tr::lng_manage_peer_reactions_all_about_channel();
case Option::Some: return isGroup
? tr::lng_manage_peer_reactions_some_about()
: tr::lng_manage_peer_reactions_some_about_channel();
case Option::None: return isGroup
? tr::lng_manage_peer_reactions_none_about()
: tr::lng_manage_peer_reactions_none_about_channel();
}
Unexpected("Option value in EditAllowedReactionsBox.");
};
Settings::AddSkip(container);
Settings::AddDividerText(
container,
(isGroup
? tr::lng_manage_peer_reactions_about
: tr::lng_manage_peer_reactions_about_channel)());
state->option.value() | rpl::map(about) | rpl::flatten_latest());
Settings::AddSkip(container);
Settings::AddSubsectionTitle(
container,
tr::lng_manage_peer_reactions_available());
const auto active = [&](const Data::Reaction &entry) {
return ranges::contains(allowed.some, entry.id);
};
const auto add = [&](const Data::Reaction &entry) {
if (entry.id.emoji().isEmpty()) {
return;
}
const auto button = Settings::AddButton(
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
wrap->toggleOn(state->option.value() | rpl::map(_1 == Option::Some));
wrap->finishAnimating();
const auto reactions = wrap->entity();
Settings::AddSkip(reactions);
Settings::AddSubsectionTitle(
reactions,
tr::lng_manage_peer_reactions_some_title());
const auto active = [&](const ReactionId &id) {
return (allowed.type != AllowedReactionsType::Some)
|| ranges::contains(allowed.some, id);
};
const auto add = [&](const Reaction &entry) {
const auto button = Settings::AddButton(
reactions,
rpl::single(entry.title),
st::manageGroupButton.button);
AddReactionAnimatedIcon(
@ -117,29 +152,41 @@ void EditAllowedReactionsBox(
}) | rpl::to_empty,
rpl::never<>(),
&button->lifetime());
state->toggles.emplace(entry.id.emoji(), button);
button->toggleOn(rpl::single(
active(entry)
) | rpl::then(
state->forceToggleAll.events()
));
button->toggledChanges(
) | rpl::start_with_next([=](bool toggled) {
if (toggled) {
state->anyToggled = true;
} else if (collect().empty()) {
state->anyToggled = false;
}
}, button->lifetime());
state->toggles.emplace(entry.id, button);
button->toggleOn(rpl::single(active(entry.id)));
};
for (const auto &entry : list) {
add(entry);
}
for (const auto &id : allowed.some) {
if (const auto customId = id.custom()) {
// Some possible forward compatibility.
const auto button = Settings::AddButton(
reactions,
rpl::single(u"Custom reaction"_q),
st::manageGroupButton.button);
AddReactionCustomIcon(
button,
button->sizeValue(
) | rpl::map([=](const QSize &size) {
return QPoint(
st::editPeerReactionsIconLeft,
(size.height() - iconHeight) / 2);
}),
iconHeight,
navigation->parentController(),
customId,
rpl::never<>(),
&button->lifetime());
state->toggles.emplace(id, button);
button->toggleOn(rpl::single(true));
}
}
box->addButton(tr::lng_settings_save(), [=] {
const auto ids = collect();
const auto result = collect();
box->closeBox();
callback(ids, false);
callback(result);
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
@ -148,16 +195,18 @@ void EditAllowedReactionsBox(
void SaveAllowedReactions(
not_null<PeerData*> peer,
const std::vector<QString> &allowed,
bool all) {
auto ids = allowed | ranges::views::transform([=](QString value) {
return MTP_reactionEmoji(MTP_string(value));
}) | ranges::to<QVector<MTPReaction>>;
const Data::AllowedReactions &allowed) {
auto ids = allowed.some | ranges::views::transform(
Data::ReactionToMTP
) | ranges::to<QVector<MTPReaction>>;
const auto updated = all
? MTP_chatReactionsAll(MTP_flags(peer->isBroadcast()
using Type = Data::AllowedReactionsType;
const auto updated = (allowed.type != Type::Some)
? MTP_chatReactionsAll(MTP_flags((allowed.type == Type::Default)
? MTPDchatReactionsAll::Flag(0)
: MTPDchatReactionsAll::Flag::f_allow_custom))
: allowed.some.empty()
? MTP_chatReactionsNone()
: MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids));
peer->session().api().request(MTPmessages_SetChatAvailableReactions(
peer->input,

View file

@ -18,14 +18,18 @@ namespace Ui {
class GenericBox;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
void EditAllowedReactionsBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
bool isGroup,
const std::vector<Data::Reaction> &list,
const Data::AllowedReactions &allowed,
Fn<void(const std::vector<QString> &, bool all)> callback);
Fn<void(const Data::AllowedReactions &)> callback);
void SaveAllowedReactions(
not_null<PeerData*> peer,
const std::vector<QString> &allowed,
bool all);
const Data::AllowedReactions &allowed);

View file

@ -251,6 +251,73 @@ void AddMessage(
}, widget->lifetime());
}
not_null<Ui::RpWidget*> AddReactionIconWrap(
not_null<Ui::RpWidget*> parent,
rpl::producer<QPoint> iconPositionValue,
int iconSize,
Fn<void(not_null<QWidget*>, QPainter&)> paintCallback,
rpl::producer<> &&destroys,
not_null<rpl::lifetime*> stateLifetime) {
struct State {
base::unique_qptr<Ui::RpWidget> widget;
Ui::Animations::Simple finalAnimation;
};
const auto state = stateLifetime->make_state<State>();
state->widget = base::make_unique_q<Ui::RpWidget>(parent);
const auto widget = state->widget.get();
widget->resize(iconSize, iconSize);
widget->setAttribute(Qt::WA_TransparentForMouseEvents);
std::move(
iconPositionValue
) | rpl::start_with_next([=](const QPoint &point) {
widget->moveToLeft(point.x(), point.y());
}, widget->lifetime());
const auto update = crl::guard(widget, [=] { widget->update(); });
widget->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(widget);
if (state->finalAnimation.animating()) {
const auto progress = 1. - state->finalAnimation.value(0.);
const auto size = widget->size();
const auto scaledSize = size * progress;
const auto scaledCenter = QPoint(
(size.width() - scaledSize.width()) / 2.,
(size.height() - scaledSize.height()) / 2.);
p.setOpacity(progress);
p.translate(scaledCenter);
p.scale(progress, progress);
}
paintCallback(widget, p);
}, widget->lifetime());
std::move(
destroys
) | rpl::take(1) | rpl::start_with_next([=, from = 0., to = 1.] {
state->finalAnimation.start(
[=](float64 value) {
update();
if (value == to) {
stateLifetime->destroy();
}
},
from,
to,
st::defaultPopupMenu.showDuration);
}, widget->lifetime());
widget->raise();
widget->show();
return widget;
}
} // namespace
void AddReactionAnimatedIcon(
@ -270,14 +337,8 @@ void AddReactionAnimatedIcon(
Entry select;
bool appearAnimated = false;
rpl::lifetime loadingLifetime;
base::unique_qptr<Ui::RpWidget> widget;
Ui::Animations::Simple finalAnimation;
};
const auto state = stateLifetime->make_state<State>();
state->widget = base::make_unique_q<Ui::RpWidget>(parent);
state->appear.media = reaction.appearAnimation->createMediaView();
state->select.media = reaction.selectAnimation->createMediaView();
@ -303,34 +364,7 @@ void AddReactionAnimatedIcon(
}
}, state->loadingLifetime);
const auto widget = state->widget.get();
widget->resize(iconSize, iconSize);
widget->setAttribute(Qt::WA_TransparentForMouseEvents);
std::move(
iconPositionValue
) | rpl::start_with_next([=](const QPoint &point) {
widget->moveToLeft(point.x(), point.y());
}, widget->lifetime());
const auto update = crl::guard(widget, [=] { widget->update(); });
widget->paintRequest(
) | rpl::start_with_next([=] {
Painter p(widget);
if (state->finalAnimation.animating()) {
const auto progress = 1. - state->finalAnimation.value(0.);
const auto size = widget->size();
const auto scaledSize = size * progress;
const auto scaledCenter = QPoint(
(size.width() - scaledSize.width()) / 2.,
(size.height() - scaledSize.height()) / 2.);
p.setOpacity(progress);
p.translate(scaledCenter);
p.scale(progress, progress);
}
const auto paintCallback = [=](not_null<QWidget*> widget, QPainter &p) {
const auto paintFrame = [&](not_null<Ui::AnimatedIcon*> animation) {
const auto frame = animation->frame();
p.drawImage(
@ -345,41 +379,72 @@ void AddReactionAnimatedIcon(
const auto appear = state->appear.icon.get();
if (appear && !state->appearAnimated) {
state->appearAnimated = true;
appear->animate(update);
appear->animate(crl::guard(widget, [=] { widget->update(); }));
}
if (appear && appear->animating()) {
paintFrame(appear);
} else if (const auto select = state->select.icon.get()) {
paintFrame(select);
}
}, widget->lifetime());
};
const auto widget = AddReactionIconWrap(
parent,
std::move(iconPositionValue),
iconSize,
paintCallback,
std::move(destroys),
stateLifetime);
std::move(
selects
) | rpl::start_with_next([=] {
const auto select = state->select.icon.get();
if (select && !select->animating()) {
select->animate(update);
select->animate(crl::guard(widget, [=] { widget->update(); }));
}
}, widget->lifetime());
}
std::move(
destroys
) | rpl::take(1) | rpl::start_with_next([=, from = 0., to = 1.] {
state->finalAnimation.start(
[=](float64 value) {
update();
if (value == to) {
stateLifetime->destroy();
}
},
from,
to,
st::defaultPopupMenu.showDuration);
}, widget->lifetime());
void AddReactionCustomIcon(
not_null<Ui::RpWidget*> parent,
rpl::producer<QPoint> iconPositionValue,
int iconSize,
not_null<Window::SessionController*> controller,
DocumentId customId,
rpl::producer<> &&destroys,
not_null<rpl::lifetime*> stateLifetime) {
struct State {
std::unique_ptr<Ui::Text::CustomEmoji> custom;
Fn<void()> repaint;
};
const auto state = stateLifetime->make_state<State>();
static constexpr auto tag = Data::CustomEmojiManager::SizeTag::Large;
state->custom = controller->session().data().customEmojiManager().create(
customId,
[=] { state->repaint(); },
tag);
widget->raise();
widget->show();
const auto paintCallback = [=](not_null<QWidget*> widget, QPainter &p) {
const auto ratio = style::DevicePixelRatio();
const auto size = Data::FrameSizeFromTag(tag) / ratio;
state->custom->paint(p, {
.preview = st::windowBgRipple->c,
.now = crl::now(),
.position = QPoint(
(widget->width() - size) / 2,
(widget->height() - size) / 2),
.paused = controller->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer),
});
};
const auto widget = AddReactionIconWrap(
parent,
std::move(iconPositionValue),
iconSize,
paintCallback,
std::move(destroys),
stateLifetime);
}
void ReactionsSettingsBox(

View file

@ -28,6 +28,14 @@ void AddReactionAnimatedIcon(
rpl::producer<> &&selects,
rpl::producer<> &&destroys,
not_null<rpl::lifetime*> stateLifetime);
void AddReactionCustomIcon(
not_null<Ui::RpWidget*> parent,
rpl::producer<QPoint> iconPositionValue,
int iconSize,
not_null<Window::SessionController*> controller,
DocumentId customId,
rpl::producer<> &&destroys,
not_null<rpl::lifetime*> stateLifetime);
void ReactionsSettingsBox(
not_null<Ui::GenericBox*> box,

View file

@ -140,6 +140,9 @@ PossibleItemReactionsRef LookupPossibleReactions(
if ((allowed.type == AllowedReactionsType::Some)
&& !ranges::contains(allowed.some, id)) {
return false;
} else if (id.custom()
&& allowed.type == AllowedReactionsType::Default) {
return false;
} else if (reaction.premium
&& !session->premium()
&& !ranges::contains(all, id, &MessageReaction::id)) {

View file

@ -455,23 +455,6 @@ rpl::producer<int> FullReactionsCountValue(
}) | rpl::distinct_until_changed();
}
rpl::producer<int> AllowedReactionsCountValue(not_null<PeerData*> peer) {
if (peer->isUser()) {
return FullReactionsCountValue(&peer->session());
}
return rpl::combine(
FullReactionsCountValue(&peer->session()),
peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Reactions)
) | rpl::map([=](int full, const auto&) {
const auto &allowed = Data::PeerAllowedReactions(peer);
return (allowed.type == Data::AllowedReactionsType::Some)
? int(allowed.some.size())
: full;
});
}
template <typename Flag, typename Peer>
rpl::producer<Badge> BadgeValueFromFlags(Peer peer) {
return rpl::combine(

View file

@ -89,8 +89,6 @@ rpl::producer<not_null<PeerData*>> MigratedOrMeValue(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<int> FullReactionsCountValue(
not_null<Main::Session*> peer);
[[nodiscard]] rpl::producer<int> AllowedReactionsCountValue(
not_null<PeerData*> peer);
enum class Badge;
[[nodiscard]] rpl::producer<Badge> BadgeValue(not_null<PeerData*> peer);