Correctly apply reaction restrictions.

This commit is contained in:
John Preston 2022-01-10 17:22:43 +03:00
parent 2733b12cff
commit 963694330d
22 changed files with 313 additions and 170 deletions

View file

@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
#include "data/data_peer_values.h"
#include "history/admin_log/history_admin_log_section.h" #include "history/admin_log/history_admin_log_section.h"
#include "info/profile/info_profile_values.h" #include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
@ -1060,7 +1061,7 @@ void Controller::fillManageSection() {
!_peer->isBroadcast(), !_peer->isBroadcast(),
session->data().reactions().list( session->data().reactions().list(
Data::Reactions::Type::Active), Data::Reactions::Type::Active),
session->data().reactions().list(_peer), *Data::PeerAllowedReactions(_peer),
done)); done));
}, },
st::infoIconReactions); st::infoIconReactions);

View file

@ -93,7 +93,7 @@ void EditAllowedReactionsBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
bool isGroup, bool isGroup,
const std::vector<Reaction> &list, const std::vector<Reaction> &list,
const std::vector<Reaction> &selected, const base::flat_set<QString> &selected,
Fn<void(const std::vector<QString> &)> callback) { Fn<void(const std::vector<QString> &)> callback) {
box->setTitle(tr::lng_manage_peer_reactions()); box->setTitle(tr::lng_manage_peer_reactions());
@ -146,7 +146,7 @@ void EditAllowedReactionsBox(
tr::lng_manage_peer_reactions_available()); tr::lng_manage_peer_reactions_available());
const auto active = [&](const Data::Reaction &entry) { const auto active = [&](const Data::Reaction &entry) {
return ranges::contains(selected, entry.emoji, &Reaction::emoji); return selected.contains(entry.emoji);
}; };
const auto add = [&](const Data::Reaction &entry) { const auto add = [&](const Data::Reaction &entry) {
const auto button = Settings::AddButton( const auto button = Settings::AddButton(
@ -198,9 +198,9 @@ void SaveAllowedReactions(
)).done([=](const MTPUpdates &result) { )).done([=](const MTPUpdates &result) {
peer->session().api().applyUpdates(result); peer->session().api().applyUpdates(result);
if (const auto chat = peer->asChat()) { if (const auto chat = peer->asChat()) {
chat->setAllowedReactions(allowed); chat->setAllowedReactions({ begin(allowed), end(allowed) });
} else if (const auto channel = peer->asChannel()) { } else if (const auto channel = peer->asChannel()) {
channel->setAllowedReactions(allowed); channel->setAllowedReactions({ begin(allowed), end(allowed) });
} else { } else {
Unexpected("Invalid peer type in SaveAllowedReactions."); Unexpected("Invalid peer type in SaveAllowedReactions.");
} }

View file

@ -19,7 +19,7 @@ void EditAllowedReactionsBox(
not_null<Ui::GenericBox*> box, not_null<Ui::GenericBox*> box,
bool isGroup, bool isGroup,
const std::vector<Data::Reaction> &list, const std::vector<Data::Reaction> &list,
const std::vector<Data::Reaction> &selected, const base::flat_set<QString> &selected,
Fn<void(const std::vector<QString> &)> callback); Fn<void(const std::vector<QString> &)> callback);
void SaveAllowedReactions( void SaveAllowedReactions(

View file

@ -761,7 +761,7 @@ PeerId ChannelData::groupCallDefaultJoinAs() const {
return _callDefaultJoinAs; return _callDefaultJoinAs;
} }
void ChannelData::setAllowedReactions(std::vector<QString> list) { void ChannelData::setAllowedReactions(base::flat_set<QString> list) {
if (_allowedReactions != list) { if (_allowedReactions != list) {
const auto toggled = (_allowedReactions.empty() != list.empty()); const auto toggled = (_allowedReactions.empty() != list.empty());
_allowedReactions = std::move(list); _allowedReactions = std::move(list);
@ -774,7 +774,7 @@ void ChannelData::setAllowedReactions(std::vector<QString> list) {
} }
} }
const std::vector<QString> &ChannelData::allowedReactions() const { const base::flat_set<QString> &ChannelData::allowedReactions() const {
return _allowedReactions; return _allowedReactions;
} }

View file

@ -410,8 +410,8 @@ public:
void setGroupCallDefaultJoinAs(PeerId peerId); void setGroupCallDefaultJoinAs(PeerId peerId);
[[nodiscard]] PeerId groupCallDefaultJoinAs() const; [[nodiscard]] PeerId groupCallDefaultJoinAs() const;
void setAllowedReactions(std::vector<QString> list); void setAllowedReactions(base::flat_set<QString> list);
[[nodiscard]] const std::vector<QString> &allowedReactions() const; [[nodiscard]] const base::flat_set<QString> &allowedReactions() const;
// Still public data members. // Still public data members.
uint64 access = 0; uint64 access = 0;
@ -460,7 +460,7 @@ private:
QString _inviteLink; QString _inviteLink;
std::optional<ChannelData*> _linkedChat; std::optional<ChannelData*> _linkedChat;
std::vector<QString> _allowedReactions; base::flat_set<QString> _allowedReactions;
std::unique_ptr<Data::GroupCall> _call; std::unique_ptr<Data::GroupCall> _call;
PeerId _callDefaultJoinAs = 0; PeerId _callDefaultJoinAs = 0;

View file

@ -287,7 +287,7 @@ void ChatData::setPendingRequestsCount(
} }
} }
void ChatData::setAllowedReactions(std::vector<QString> list) { void ChatData::setAllowedReactions(base::flat_set<QString> list) {
if (_allowedReactions != list) { if (_allowedReactions != list) {
const auto toggled = (_allowedReactions.empty() != list.empty()); const auto toggled = (_allowedReactions.empty() != list.empty());
_allowedReactions = std::move(list); _allowedReactions = std::move(list);
@ -300,7 +300,7 @@ void ChatData::setAllowedReactions(std::vector<QString> list) {
} }
} }
const std::vector<QString> &ChatData::allowedReactions() const { const base::flat_set<QString> &ChatData::allowedReactions() const {
return _allowedReactions; return _allowedReactions;
} }

View file

@ -175,8 +175,8 @@ public:
int count, int count,
std::vector<UserId> recentRequesters); std::vector<UserId> recentRequesters);
void setAllowedReactions(std::vector<QString> list); void setAllowedReactions(base::flat_set<QString> list);
[[nodiscard]] const std::vector<QString> &allowedReactions() const; [[nodiscard]] const base::flat_set<QString> &allowedReactions() const;
// Still public data members. // Still public data members.
const MTPlong inputChat; const MTPlong inputChat;
@ -202,7 +202,7 @@ private:
int _pendingRequestsCount = 0; int _pendingRequestsCount = 0;
std::vector<UserId> _recentRequesters; std::vector<UserId> _recentRequesters;
std::vector<QString> _allowedReactions; base::flat_set<QString> _allowedReactions;
std::unique_ptr<Data::GroupCall> _call; std::unique_ptr<Data::GroupCall> _call;
PeerId _callDefaultJoinAs = 0; PeerId _callDefaultJoinAs = 0;

View file

@ -11,11 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item.h" #include "history/history_item.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_channel.h" #include "data/data_changes.h"
#include "data/data_chat.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_changes.h"
#include "lottie/lottie_icon.h" #include "lottie/lottie_icon.h"
#include "base/timer_rpl.h" #include "base/timer_rpl.h"
#include "apiwrap.h" #include "apiwrap.h"
@ -65,16 +63,6 @@ const std::vector<Reaction> &Reactions::list(Type type) const {
Unexpected("Type in Reactions::list."); Unexpected("Type in Reactions::list.");
} }
std::vector<Reaction> Reactions::list(not_null<PeerData*> peer) const {
if (const auto chat = peer->asChat()) {
return filtered(chat->allowedReactions());
} else if (const auto channel = peer->asChannel()) {
return filtered(channel->allowedReactions());
} else {
return list(Type::Active);
}
}
rpl::producer<> Reactions::updates() const { rpl::producer<> Reactions::updates() const {
return _updated.events(); return _updated.events();
} }
@ -214,33 +202,17 @@ void Reactions::downloadTaskFinished() {
} }
} }
std::vector<Reaction> Reactions::Filtered( base::flat_set<QString> Reactions::ParseAllowed(
const std::vector<Reaction> &reactions,
const std::vector<QString> &emoji) {
auto result = std::vector<Reaction>();
result.reserve(emoji.size());
for (const auto &single : emoji) {
const auto i = ranges::find(reactions, single, &Reaction::emoji);
if (i != end(reactions)) {
result.push_back(*i);
}
}
return result;
}
std::vector<Reaction> Reactions::filtered(
const std::vector<QString> &emoji) const {
return Filtered(list(Type::Active), emoji);
}
std::vector<QString> Reactions::ParseAllowed(
const MTPVector<MTPstring> *list) { const MTPVector<MTPstring> *list) {
if (!list) { if (!list) {
return {}; return {};
} }
return list->v | ranges::view::transform([](const MTPstring &string) { const auto parsed = ranges::views::all(
list->v
) | ranges::views::transform([](const MTPstring &string) {
return qs(string); return qs(string);
}) | ranges::to_vector; }) | ranges::to_vector;
return { begin(parsed), end(parsed) };
} }
void Reactions::request() { void Reactions::request() {

View file

@ -43,15 +43,8 @@ public:
All, All,
}; };
[[nodiscard]] const std::vector<Reaction> &list(Type type) const; [[nodiscard]] const std::vector<Reaction> &list(Type type) const;
[[nodiscard]] std::vector<Reaction> list(not_null<PeerData*> peer) const;
[[nodiscard]] static std::vector<Reaction> Filtered( [[nodiscard]] static base::flat_set<QString> ParseAllowed(
const std::vector<Reaction> &reactions,
const std::vector<QString> &emoji);
[[nodiscard]] std::vector<Reaction> filtered(
const std::vector<QString> &emoji) const;
[[nodiscard]] static std::vector<QString> ParseAllowed(
const MTPVector<MTPstring> *list); const MTPVector<MTPstring> *list);
[[nodiscard]] rpl::producer<> updates() const; [[nodiscard]] rpl::producer<> updates() const;

View file

@ -15,6 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "ui/image/image_prepare.h" #include "ui/image/image_prepare.h"
#include "base/unixtime.h" #include "base/unixtime.h"
@ -497,18 +499,36 @@ rpl::producer<QImage> PeerUserpicImageValue(
}; };
} }
rpl::producer<std::vector<Data::Reaction>> PeerAllowedReactionsValue( std::optional<base::flat_set<QString>> PeerAllowedReactions(
not_null<PeerData*> peer) { not_null<PeerData*> peer) {
return rpl::combine( if (const auto chat = peer->asChat()) {
rpl::single( return chat->allowedReactions();
rpl::empty_value() } else if (const auto channel = peer->asChannel()) {
) | rpl::then(peer->owner().reactions().updates()), return channel->allowedReactions();
peer->session().changes().peerFlagsValue( } else {
peer, return std::nullopt;
Data::PeerUpdate::Flag::Reactions) }
) | rpl::map([=] { }
return peer->owner().reactions().list(peer);
auto PeerAllowedReactionsValue(
not_null<PeerData*> peer)
-> rpl::producer<std::optional<base::flat_set<QString>>> {
return peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::Reactions
) | rpl::map([=]{
return PeerAllowedReactions(peer);
}); });
} }
rpl::producer<int> UniqueReactionsLimitValue(
not_null<Main::Session*> session) {
const auto config = &session->account().appConfig();
return config->value(
) | rpl::map([=] {
return int(base::SafeRound(
config->get<double>("reactions_uniq_max", 11)));
}) | rpl::distinct_until_changed();
}
} // namespace Data } // namespace Data

View file

@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
enum class ImageRoundRadius; enum class ImageRoundRadius;
namespace Main {
class Session;
} // namespace Main
namespace Data { namespace Data {
struct Reaction; struct Reaction;
@ -122,7 +126,12 @@ inline auto PeerFullFlagValue(
int size, int size,
ImageRoundRadius radius); ImageRoundRadius radius);
[[nodiscard]] std::optional<base::flat_set<QString>> PeerAllowedReactions(
not_null<PeerData*> peer);
[[nodiscard]] auto PeerAllowedReactionsValue(not_null<PeerData*> peer) [[nodiscard]] auto PeerAllowedReactionsValue(not_null<PeerData*> peer)
-> rpl::producer<std::vector<Data::Reaction>>; -> rpl::producer<std::optional<base::flat_set<QString>>>;
[[nodiscard]] rpl::producer<int> UniqueReactionsLimitValue(
not_null<Main::Session*> session);
} // namespace Data } // namespace Data

View file

@ -322,6 +322,7 @@ HistoryInner::HistoryInner(
, _reactionsManager( , _reactionsManager(
std::make_unique<HistoryView::Reactions::Manager>( std::make_unique<HistoryView::Reactions::Manager>(
this, this,
Data::UniqueReactionsLimitValue(&controller->session()),
[=](QRect updated) { update(updated); })) [=](QRect updated) { update(updated); }))
, _touchSelectTimer([=] { onTouchSelect(); }) , _touchSelectTimer([=] { onTouchSelect(); })
, _touchScrollTimer([=] { onTouchScrollTimer(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); })
@ -412,6 +413,7 @@ HistoryInner::HistoryInner(
return item->mainView() != nullptr; return item->mainView() != nullptr;
}) | rpl::start_with_next([=](not_null<HistoryItem*> item) { }) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
item->mainView()->itemDataChanged(); item->mainView()->itemDataChanged();
_reactionsManager->updateUniqueLimit(item);
}, lifetime()); }, lifetime());
session().changes().historyUpdates( session().changes().historyUpdates(
@ -421,11 +423,10 @@ HistoryInner::HistoryInner(
update(); update();
}, lifetime()); }, lifetime());
Data::PeerAllowedReactionsValue( HistoryView::Reactions::SetupManagerList(
_peer _reactionsManager.get(),
) | rpl::start_with_next([=](std::vector<Data::Reaction> &&list) { &session(),
_reactionsManager->applyList(std::move(list)); Data::PeerAllowedReactionsValue(_peer));
}, lifetime());
controller->adaptive().chatWideValue( controller->adaptive().chatWideValue(
) | rpl::start_with_next([=](bool wide) { ) | rpl::start_with_next([=](bool wide) {
@ -3169,7 +3170,8 @@ void HistoryInner::mouseActionUpdate() {
: nullptr; : nullptr;
const auto item = view ? view->data().get() : nullptr; const auto item = view ? view->data().get() : nullptr;
if (view) { if (view) {
if (App::mousedItem() != view) { const auto changed = (App::mousedItem() != view);
if (changed) {
repaintItem(App::mousedItem()); repaintItem(App::mousedItem());
App::mousedItem(view); App::mousedItem(view);
repaintItem(App::mousedItem()); repaintItem(App::mousedItem());
@ -3179,6 +3181,9 @@ void HistoryInner::mouseActionUpdate() {
view, view,
m, m,
reactionState)); reactionState));
if (changed) {
_reactionsManager->updateUniqueLimit(item);
}
if (view->pointState(m) != PointState::Outside) { if (view->pointState(m) != PointState::Outside) {
if (App::hoveredItem() != view) { if (App::hoveredItem() != view) {
repaintItem(App::hoveredItem()); repaintItem(App::hoveredItem());

View file

@ -49,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_file_click_handler.h" #include "data/data_file_click_handler.h"
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
#include "data/data_peer_values.h"
#include "facades.h" #include "facades.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
@ -267,6 +268,7 @@ ListWidget::ListWidget(
, _reactionsManager( , _reactionsManager(
std::make_unique<Reactions::Manager>( std::make_unique<Reactions::Manager>(
this, this,
Data::UniqueReactionsLimitValue(&controller->session()),
[=](QRect updated) { update(updated); })) [=](QRect updated) { update(updated); }))
, _scrollDateCheck([this] { scrollDateCheck(); }) , _scrollDateCheck([this] { scrollDateCheck(); })
, _applyUpdatedScrollState([this] { applyUpdatedScrollState(); }) , _applyUpdatedScrollState([this] { applyUpdatedScrollState(); })
@ -344,10 +346,10 @@ ListWidget::ListWidget(
} }
}, lifetime()); }, lifetime());
_delegate->listAllowedReactionsValue( Reactions::SetupManagerList(
) | rpl::start_with_next([=](std::vector<Data::Reaction> &&list) { _reactionsManager.get(),
_reactionsManager->applyList(std::move(list)); &session(),
}, lifetime()); _delegate->listAllowedReactionsValue());
controller->adaptive().chatWideValue( controller->adaptive().chatWideValue(
) | rpl::start_with_next([=](bool wide) { ) | rpl::start_with_next([=](bool wide) {
@ -2563,7 +2565,8 @@ void ListWidget::mouseActionUpdate() {
view ? view->height() : 0, view ? view->height() : 0,
itemPoint, itemPoint,
view ? view->pointState(itemPoint) : PointState::Outside); view ? view->pointState(itemPoint) : PointState::Outside);
if (_overElement != view) { const auto viewChanged = (_overElement != view);
if (viewChanged) {
repaintItem(_overElement); repaintItem(_overElement);
_overElement = view; _overElement = view;
repaintItem(_overElement); repaintItem(_overElement);
@ -2574,6 +2577,9 @@ void ListWidget::mouseActionUpdate() {
itemPoint, itemPoint,
reactionState) reactionState)
: Reactions::ButtonParameters()); : Reactions::ButtonParameters());
if (viewChanged && view) {
_reactionsManager->updateUniqueLimit(item);
}
TextState dragState; TextState dragState;
ClickHandlerHost *lnkhost = nullptr; ClickHandlerHost *lnkhost = nullptr;

View file

@ -113,7 +113,7 @@ public:
} }
virtual CopyRestrictionType listSelectRestrictionType() = 0; virtual CopyRestrictionType listSelectRestrictionType() = 0;
virtual auto listAllowedReactionsValue() virtual auto listAllowedReactionsValue()
-> rpl::producer<std::vector<Data::Reaction>> = 0; -> rpl::producer<std::optional<base::flat_set<QString>>> = 0;
}; };
struct SelectionData { struct SelectionData {

View file

@ -684,7 +684,7 @@ CopyRestrictionType PinnedWidget::listSelectRestrictionType() {
} }
auto PinnedWidget::listAllowedReactionsValue() auto PinnedWidget::listAllowedReactionsValue()
-> rpl::producer<std::vector<Data::Reaction>> { -> rpl::producer<std::optional<base::flat_set<QString>>> {
return Data::PeerAllowedReactionsValue(_history->peer); return Data::PeerAllowedReactionsValue(_history->peer);
} }

View file

@ -106,7 +106,7 @@ public:
CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override; CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override;
CopyRestrictionType listSelectRestrictionType() override; CopyRestrictionType listSelectRestrictionType() override;
auto listAllowedReactionsValue() auto listAllowedReactionsValue()
-> rpl::producer<std::vector<Data::Reaction>> override; -> rpl::producer<std::optional<base::flat_set<QString>>> override;
protected: protected:
void resizeEvent(QResizeEvent *e) override; void resizeEvent(QResizeEvent *e) override;

View file

@ -8,9 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_react_button.h" #include "history/view/history_view_react_button.h"
#include "history/view/history_view_cursor_state.h" #include "history/view/history_view_cursor_state.h"
#include "history/history_item.h"
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
#include "ui/chat/message_bubble.h" #include "ui/chat/message_bubble.h"
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_document_media.h" #include "data/data_document_media.h"
#include "lottie/lottie_icon.h" #include "lottie/lottie_icon.h"
@ -70,7 +72,6 @@ constexpr auto kHoverScale = 1.24;
[[nodiscard]] std::shared_ptr<Lottie::Icon> CreateIcon( [[nodiscard]] std::shared_ptr<Lottie::Icon> CreateIcon(
not_null<Data::DocumentMedia*> media, not_null<Data::DocumentMedia*> media,
int startFrame,
int size) { int size) {
Expects(media->loaded()); Expects(media->loaded());
@ -78,7 +79,6 @@ constexpr auto kHoverScale = 1.24;
.path = media->owner()->filepath(true), .path = media->owner()->filepath(true),
.json = media->bytes(), .json = media->bytes(),
.sizeOverride = QSize(size, size), .sizeOverride = QSize(size, size),
.frame = startFrame,
}); });
} }
@ -331,6 +331,7 @@ float64 Button::currentOpacity() const {
Manager::Manager( Manager::Manager(
QWidget *wheelEventsTarget, QWidget *wheelEventsTarget,
rpl::producer<int> uniqueLimitValue,
Fn<void(QRect)> buttonUpdate) Fn<void(QRect)> buttonUpdate)
: _outer(CountOuterSize()) : _outer(CountOuterSize())
, _inner(QRect({}, st::reactionCornerSize)) , _inner(QRect({}, st::reactionCornerSize))
@ -338,6 +339,7 @@ Manager::Manager(
QRect(0, 0, _inner.width(), _inner.width()).marginsAdded( QRect(0, 0, _inner.width(), _inner.width()).marginsAdded(
st::reactionCornerShadow st::reactionCornerShadow
).size()) ).size())
, _uniqueLimit(std::move(uniqueLimitValue))
, _buttonShowTimer([=] { showButtonDelayed(); }) , _buttonShowTimer([=] { showButtonDelayed(); })
, _buttonUpdate(std::move(buttonUpdate)) { , _buttonUpdate(std::move(buttonUpdate)) {
static_assert(!(kFramesCount % kDivider)); static_assert(!(kFramesCount % kDivider));
@ -384,6 +386,11 @@ Manager::Manager(
stealWheelEvents(wheelEventsTarget); stealWheelEvents(wheelEventsTarget);
} }
_uniqueLimit.changes(
) | rpl::start_with_next([=] {
applyListFilters();
}, _lifetime);
_createChooseCallback = [=](QString emoji) { _createChooseCallback = [=](QString emoji) {
return [=] { return [=] {
if (const auto context = _buttonContext) { if (const auto context = _buttonContext) {
@ -397,6 +404,33 @@ Manager::Manager(
}; };
} }
void Manager::applyListFilters() {
const auto limit = _uniqueLimit.current();
const auto applyUniqueLimit = _buttonContext
&& (limit > 0)
&& (_buttonAlreadyNotMineCount >= limit);
auto icons = std::vector<not_null<ReactionIcons*>>();
icons.reserve(_list.size());
for (auto &icon : _list) {
const auto add = applyUniqueLimit
? _buttonAlreadyList.contains(icon.emoji)
: (!_filter || _filter->contains(icon.emoji));
if (add) {
icons.push_back(&icon);
} else {
clearStateForHidden(icon);
}
}
if (_icons == icons) {
return;
}
const auto selected = _selectedIcon;
setSelectedIcon(-1);
_icons = std::move(icons);
setSelectedIcon(selected < _icons.size() ? selected : -1);
resolveMainReactionIcon();
}
void Manager::stealWheelEvents(not_null<QWidget*> target) { void Manager::stealWheelEvents(not_null<QWidget*> target) {
base::install_event_filter(target, [=](not_null<QEvent*> e) { base::install_event_filter(target, [=](not_null<QEvent*> e) {
if (e->type() != QEvent::Wheel if (e->type() != QEvent::Wheel
@ -422,8 +456,8 @@ void Manager::updateButton(ButtonParameters parameters) {
_scheduledParameters = std::nullopt; _scheduledParameters = std::nullopt;
} }
_buttonContext = parameters.context; _buttonContext = parameters.context;
parameters.reactionsCount = _list.size(); parameters.reactionsCount = _icons.size();
if (!_buttonContext || _list.empty()) { if (!_buttonContext || _icons.empty()) {
return; return;
} else if (_button) { } else if (_button) {
_button->applyParameters(parameters); _button->applyParameters(parameters);
@ -455,7 +489,7 @@ void Manager::showButtonDelayed() {
[=]{ updateButton({}); }); [=]{ updateButton({}); });
} }
void Manager::applyList(std::vector<Data::Reaction> list) { void Manager::applyList(const std::vector<Data::Reaction> &list) {
constexpr auto predicate = []( constexpr auto predicate = [](
const Data::Reaction &a, const Data::Reaction &a,
const Data::Reaction &b) { const Data::Reaction &b) {
@ -463,32 +497,87 @@ void Manager::applyList(std::vector<Data::Reaction> list) {
&& (a.appearAnimation == b.appearAnimation) && (a.appearAnimation == b.appearAnimation)
&& (a.selectAnimation == b.selectAnimation); && (a.selectAnimation == b.selectAnimation);
}; };
if (ranges::equal(_list, list, predicate)) { const auto proj = [](const auto &obj) {
return std::tie(obj.emoji, obj.appearAnimation, obj.selectAnimation);
};
if (ranges::equal(_list, list, ranges::equal_to(), proj, proj)) {
return; return;
} }
_list = std::move(list); const auto selected = _selectedIcon;
_links = std::vector<ClickHandlerPtr>(_list.size()); setSelectedIcon(-1);
if (_list.empty()) { _icons.clear();
_list.clear();
for (const auto &reaction : list) {
_list.push_back({
.emoji = reaction.emoji,
.appearAnimation = reaction.appearAnimation,
.selectAnimation = reaction.selectAnimation,
});
}
applyListFilters();
setSelectedIcon(selected < _icons.size() ? selected : -1);
}
void Manager::updateAllowedSublist(
std::optional<base::flat_set<QString>> filter) {
if (_filter == filter) {
return;
}
_filter = std::move(filter);
applyListFilters();
}
void Manager::updateUniqueLimit(not_null<HistoryItem*> item) {
if (item->fullId() != _buttonContext) {
return;
}
const auto &all = item->reactions();
const auto my = item->chosenReaction();
auto list = base::flat_set<QString>();
list.reserve(all.size());
auto myIsUnique = false;
for (const auto &[emoji, count] : all) {
list.emplace(emoji);
if (count == 1 && emoji == my) {
myIsUnique = true;
}
}
const auto notMineCount = int(list.size()) - (myIsUnique ? 1 : 0);
auto changed = false;
if (_buttonAlreadyList != list) {
_buttonAlreadyList = std::move(list);
changed = true;
}
if (_buttonAlreadyNotMineCount != notMineCount) {
_buttonAlreadyNotMineCount = notMineCount;
changed = true;
}
if (changed) {
applyListFilters();
}
}
void Manager::resolveMainReactionIcon() {
if (_icons.empty()) {
_mainReactionMedia = nullptr; _mainReactionMedia = nullptr;
_mainReactionLifetime.destroy(); _mainReactionLifetime.destroy();
setSelectedIcon(-1);
_icons.clear();
return; return;
} }
const auto main = _list.front().appearAnimation; const auto main = _icons.front()->selectAnimation;
if (_mainReactionMedia _icons.front()->appearAnimated = true;
&& _mainReactionMedia->owner() == main) { if (_mainReactionMedia && _mainReactionMedia->owner() == main) {
if (!_mainReactionLifetime) { if (!_mainReactionLifetime) {
loadIcons(); loadIcons();
} }
return; return;
} }
_mainReactionLifetime.destroy();
_mainReactionMedia = main->createMediaView(); _mainReactionMedia = main->createMediaView();
_mainReactionMedia->checkStickerLarge(); _mainReactionMedia->checkStickerLarge();
if (_mainReactionMedia->loaded()) { if (_mainReactionMedia->loaded()) {
_mainReactionLifetime.destroy();
setMainReactionIcon(); setMainReactionIcon();
} else { } else if (!_mainReactionLifetime) {
main->session().downloaderTaskFinished( main->session().downloaderTaskFinished(
) | rpl::filter([=] { ) | rpl::filter([=] {
return _mainReactionMedia->loaded(); return _mainReactionMedia->loaded();
@ -506,16 +595,15 @@ void Manager::setMainReactionIcon() {
const auto i = _loadCache.find(_mainReactionMedia->owner()); const auto i = _loadCache.find(_mainReactionMedia->owner());
if (i != end(_loadCache) && i->second.icon) { if (i != end(_loadCache) && i->second.icon) {
const auto &icon = i->second.icon; const auto &icon = i->second.icon;
if (icon->frameIndex() == icon->framesCount() - 1 if (!icon->frameIndex() && icon->width() == MainReactionSize()) {
&& icon->width() == MainReactionSize()) {
_mainReactionImage = i->second.icon->frame(); _mainReactionImage = i->second.icon->frame();
return; return;
} }
} }
_mainReactionImage = CreateIcon( _mainReactionImage = QImage();
_mainReactionIcon = CreateIcon(
_mainReactionMedia.get(), _mainReactionMedia.get(),
-1, MainReactionSize());
MainReactionSize())->frame();
} }
QMargins Manager::innerMargins() const { QMargins Manager::innerMargins() const {
@ -544,7 +632,7 @@ bool Manager::checkIconLoaded(ReactionDocument &entry) const {
const auto size = (entry.media == _mainReactionMedia) const auto size = (entry.media == _mainReactionMedia)
? MainReactionSize() ? MainReactionSize()
: CornerImageSize(1.); : CornerImageSize(1.);
entry.icon = CreateIcon(entry.media.get(), entry.startFrame, size); entry.icon = CreateIcon(entry.media.get(), size);
entry.media = nullptr; entry.media = nullptr;
return true; return true;
} }
@ -556,14 +644,13 @@ void Manager::updateCurrentButton() const {
} }
void Manager::loadIcons() { void Manager::loadIcons() {
const auto load = [&](not_null<DocumentData*> document, int frame) { const auto load = [&](not_null<DocumentData*> document) {
if (const auto i = _loadCache.find(document); i != end(_loadCache)) { if (const auto i = _loadCache.find(document); i != end(_loadCache)) {
return i->second.icon; return i->second.icon;
} }
auto &entry = _loadCache.emplace(document).first->second; auto &entry = _loadCache.emplace(document).first->second;
entry.media = document->createMediaView(); entry.media = document->createMediaView();
entry.media->checkStickerLarge(); entry.media->checkStickerLarge();
entry.startFrame = frame;
if (!checkIconLoaded(entry) && !_loadCacheLifetime) { if (!checkIconLoaded(entry) && !_loadCacheLifetime) {
document->session().downloaderTaskFinished( document->session().downloaderTaskFinished(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
@ -572,19 +659,14 @@ void Manager::loadIcons() {
} }
return entry.icon; return entry.icon;
}; };
const auto selected = _selectedIcon; for (const auto &icon : _icons) {
setSelectedIcon(-1); if (!icon->appear) {
_icons.clear(); icon->appear = load(icon->appearAnimation);
auto main = true; }
for (const auto &reaction : _list) { if (!icon->select) {
_icons.push_back({ icon->select = load(icon->selectAnimation);
.appear = load(reaction.appearAnimation, main ? -1 : 0), }
.select = load(reaction.selectAnimation, 0),
.appearAnimated = main,
});
main = false;
} }
setSelectedIcon(selected < _icons.size() ? selected : -1);
} }
void Manager::checkIcons() { void Manager::checkIcons() {
@ -617,7 +699,7 @@ void Manager::paintButtons(Painter &p, const PaintContext &context) {
} }
ClickHandlerPtr Manager::computeButtonLink(QPoint position) const { ClickHandlerPtr Manager::computeButtonLink(QPoint position) const {
if (_list.empty()) { if (_icons.empty()) {
setSelectedIcon(-1); setSelectedIcon(-1);
return nullptr; return nullptr;
} }
@ -631,10 +713,10 @@ ClickHandlerPtr Manager::computeButtonLink(QPoint position) const {
const auto index = std::clamp( const auto index = std::clamp(
int(base::SafeRound(shifted + between / 2.)) / oneHeight, int(base::SafeRound(shifted + between / 2.)) / oneHeight,
0, 0,
int(_list.size() - 1)); int(_icons.size() - 1));
auto &result = _links[index]; auto &result = _icons[index]->link;
if (!result) { if (!result) {
result = resolveButtonLink(_list[index]); result = resolveButtonLink(*_icons[index]);
} }
setSelectedIcon(index); setSelectedIcon(index);
return result; return result;
@ -645,25 +727,25 @@ void Manager::setSelectedIcon(int index) const {
if (index < 0 || index >= _icons.size()) { if (index < 0 || index >= _icons.size()) {
return; return;
} }
auto &icon = _icons[index]; const auto &icon = _icons[index];
if (icon.selected == selected) { if (icon->selected == selected) {
return; return;
} }
icon.selected = selected; icon->selected = selected;
icon.selectedScale.start( icon->selectedScale.start(
[=] { updateCurrentButton(); }, [=] { updateCurrentButton(); },
selected ? 1. : kHoverScale, selected ? 1. : kHoverScale,
selected ? kHoverScale : 1., selected ? kHoverScale : 1.,
kHoverScaleDuration, kHoverScaleDuration,
anim::sineInOut); anim::sineInOut);
if (selected) { if (selected) {
const auto skipAnimation = icon.selectAnimated const auto skipAnimation = icon->selectAnimated
|| !icon.appearAnimated || !icon->appearAnimated
|| (icon.select && icon.select->animating()) || (icon->select && icon->select->animating())
|| (icon.appear && icon.appear->animating()); || (icon->appear && icon->appear->animating());
const auto select = skipAnimation ? nullptr : icon.select.get(); const auto select = skipAnimation ? nullptr : icon->select.get();
if (select && !icon.selectAnimated) { if (select && !icon->selectAnimated) {
icon.selectAnimated = true; icon->selectAnimated = true;
select->animate( select->animate(
[=] { updateCurrentButton(); }, [=] { updateCurrentButton(); },
0, 0,
@ -679,7 +761,7 @@ void Manager::setSelectedIcon(int index) const {
} }
ClickHandlerPtr Manager::resolveButtonLink( ClickHandlerPtr Manager::resolveButtonLink(
const Data::Reaction &reaction) const { const ReactionIcons &reaction) const {
const auto emoji = reaction.emoji; const auto emoji = reaction.emoji;
const auto i = _reactionsLinks.find(emoji); const auto i = _reactionsLinks.find(emoji);
if (i != end(_reactionsLinks)) { if (i != end(_reactionsLinks)) {
@ -1043,12 +1125,12 @@ bool Manager::onlyMainEmojiVisible() const {
return true; return true;
} }
const auto &icon = _icons.front(); const auto &icon = _icons.front();
if (icon.selected if (icon->selected
|| icon.selectedScale.animating() || icon->selectedScale.animating()
|| (icon.select && icon.select->animating())) { || (icon->select && icon->select->animating())) {
return false; return false;
} }
icon.selectAnimated = false; icon->selectAnimated = false;
return true; return true;
} }
@ -1060,22 +1142,20 @@ void Manager::clearAppearAnimations() {
auto main = true; auto main = true;
for (auto &icon : _icons) { for (auto &icon : _icons) {
if (!main) { if (!main) {
if (icon.selected) { if (icon->selected) {
setSelectedIcon(-1); setSelectedIcon(-1);
} }
icon.selectedScale.stop(); icon->selectedScale.stop();
if (const auto select = icon.select.get()) { if (const auto select = icon->select.get()) {
select->jumpTo(0, nullptr); select->jumpTo(0, nullptr);
} }
icon.selectAnimated = false; icon->selectAnimated = false;
} }
if (icon.appearAnimated != main) { if (icon->appearAnimated != main) {
if (const auto appear = icon.appear.get()) { if (const auto appear = icon->appear.get()) {
appear->jumpTo( appear->jumpTo(0, nullptr);
main ? (appear->framesCount() - 1) : 0,
nullptr);
} }
icon.appearAnimated = main; icon->appearAnimated = main;
} }
main = false; main = false;
} }
@ -1160,8 +1240,8 @@ void Manager::paintAllEmoji(
const auto update = [=] { const auto update = [=] {
updateCurrentButton(); updateCurrentButton();
}; };
for (auto &icon : _icons) { for (const auto &icon : _icons) {
const auto target = countTarget(icon).translated(emojiPosition); const auto target = countTarget(*icon).translated(emojiPosition);
emojiPosition += shift; emojiPosition += shift;
const auto paintFrame = [&](not_null<Lottie::Icon*> animation) { const auto paintFrame = [&](not_null<Lottie::Icon*> animation) {
@ -1172,21 +1252,25 @@ void Manager::paintAllEmoji(
if (!target.intersects(clip)) { if (!target.intersects(clip)) {
if (current) { if (current) {
clearStateForHidden(icon); clearStateForHidden(*icon);
} }
} else if (icon.select && icon.select->animating()) { } else {
paintFrame(icon.select.get()); const auto appear = icon->appear.get();
} else if (const auto appear = icon.appear.get()) {
if (current if (current
&& !icon.appearAnimated && appear
&& !icon->appearAnimated
&& target.intersects(animationRect)) { && target.intersects(animationRect)) {
icon.appearAnimated = true; icon->appearAnimated = true;
appear->animate(update, 0, appear->framesCount() - 1); appear->animate(update, 0, appear->framesCount() - 1);
} }
paintFrame(appear); if (appear && appear->animating()) {
paintFrame(appear);
} else if (const auto select = icon->select.get()) {
paintFrame(select);
}
} }
if (current) { if (current) {
clearStateForSelectFinished(icon); clearStateForSelectFinished(*icon);
} }
} }
} }
@ -1288,6 +1372,10 @@ QRect Manager::validateEmoji(int frameIndex, float64 scale) {
const auto position = result.topLeft() / ratio; const auto position = result.topLeft() / ratio;
p.setCompositionMode(QPainter::CompositionMode_Source); p.setCompositionMode(QPainter::CompositionMode_Source);
p.fillRect(QRect(position, result.size() / ratio), Qt::transparent); p.fillRect(QRect(position, result.size() / ratio), Qt::transparent);
if (_mainReactionImage.isNull()
&& _mainReactionIcon) {
_mainReactionImage = base::take(_mainReactionIcon)->frame();
}
if (!_mainReactionImage.isNull()) { if (!_mainReactionImage.isNull()) {
const auto size = CornerImageSize(scale); const auto size = CornerImageSize(scale);
const auto inner = _inner.translated(position); const auto inner = _inner.translated(position);
@ -1355,4 +1443,25 @@ QRect Manager::validateFrame(
return result; return result;
} }
void SetupManagerList(
not_null<Manager*> manager,
not_null<Main::Session*> session,
rpl::producer<std::optional<base::flat_set<QString>>> filter) {
rpl::single(
rpl::empty_value()
) | rpl::then(
session->data().reactions().updates()
) | rpl::start_with_next([=] {
manager->applyList(
session->data().reactions().list(Data::Reactions::Type::Active));
}, manager->lifetime());
std::move(
filter
) | rpl::start_with_next([=](
std::optional<base::flat_set<QString>> &&list) {
manager->updateAllowedSublist(std::move(list));
}, manager->lifetime());
}
} // namespace HistoryView } // namespace HistoryView

View file

@ -24,6 +24,10 @@ using PaintContext = Ui::ChatPaintContext;
struct TextState; struct TextState;
} // namespace HistoryView } // namespace HistoryView
namespace Main {
class Session;
} // namespace Main
namespace Lottie { namespace Lottie {
class Icon; class Icon;
} // namespace Lottie } // namespace Lottie
@ -127,10 +131,13 @@ class Manager final : public base::has_weak_ptr {
public: public:
Manager( Manager(
QWidget *wheelEventsTarget, QWidget *wheelEventsTarget,
rpl::producer<int> uniqueLimitValue,
Fn<void(QRect)> buttonUpdate); Fn<void(QRect)> buttonUpdate);
~Manager(); ~Manager();
void applyList(std::vector<Data::Reaction> list); void applyList(const std::vector<Data::Reaction> &list);
void updateAllowedSublist(std::optional<base::flat_set<QString>> filter);
void updateUniqueLimit(not_null<HistoryItem*> item);
void updateButton(ButtonParameters parameters); void updateButton(ButtonParameters parameters);
void paintButtons(Painter &p, const PaintContext &context); void paintButtons(Painter &p, const PaintContext &context);
@ -147,15 +154,22 @@ public:
return _chosen.events(); return _chosen.events();
} }
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private: private:
struct ReactionDocument { struct ReactionDocument {
std::shared_ptr<Data::DocumentMedia> media; std::shared_ptr<Data::DocumentMedia> media;
std::shared_ptr<Lottie::Icon> icon; std::shared_ptr<Lottie::Icon> icon;
int startFrame = 0;
}; };
struct ReactionIcons { struct ReactionIcons {
QString emoji;
not_null<DocumentData*> appearAnimation;
not_null<DocumentData*> selectAnimation;
std::shared_ptr<Lottie::Icon> appear; std::shared_ptr<Lottie::Icon> appear;
std::shared_ptr<Lottie::Icon> select; std::shared_ptr<Lottie::Icon> select;
mutable ClickHandlerPtr link;
mutable Ui::Animations::Simple selectedScale; mutable Ui::Animations::Simple selectedScale;
bool appearAnimated = false; bool appearAnimated = false;
mutable bool selected = false; mutable bool selected = false;
@ -167,6 +181,7 @@ private:
}; };
static constexpr auto kFramesCount = 32; static constexpr auto kFramesCount = 32;
void applyListFilters();
void showButtonDelayed(); void showButtonDelayed();
void stealWheelEvents(not_null<QWidget*> target); void stealWheelEvents(not_null<QWidget*> target);
@ -208,6 +223,7 @@ private:
const QImage &image, const QImage &image,
QRect source); QRect source);
void resolveMainReactionIcon();
void setMainReactionIcon(); void setMainReactionIcon();
void clearAppearAnimations(); void clearAppearAnimations();
[[nodiscard]] QRect cacheRect(int frameIndex, int columnIndex) const; [[nodiscard]] QRect cacheRect(int frameIndex, int columnIndex) const;
@ -249,7 +265,7 @@ private:
[[nodiscard]] ClickHandlerPtr computeButtonLink(QPoint position) const; [[nodiscard]] ClickHandlerPtr computeButtonLink(QPoint position) const;
[[nodiscard]] ClickHandlerPtr resolveButtonLink( [[nodiscard]] ClickHandlerPtr resolveButtonLink(
const Data::Reaction &reaction) const; const ReactionIcons &reaction) const;
void updateCurrentButton() const; void updateCurrentButton() const;
[[nodiscard]] bool onlyMainEmojiVisible() const; [[nodiscard]] bool onlyMainEmojiVisible() const;
@ -258,8 +274,8 @@ private:
void checkIcons(); void checkIcons();
rpl::event_stream<Chosen> _chosen; rpl::event_stream<Chosen> _chosen;
std::vector<Data::Reaction> _list; std::vector<ReactionIcons> _list;
mutable std::vector<ClickHandlerPtr> _links; std::optional<base::flat_set<QString>> _filter;
QSize _outer; QSize _outer;
QRect _inner; QRect _inner;
QSize _overlayFull; QSize _overlayFull;
@ -282,11 +298,13 @@ private:
QColor _shadow; QColor _shadow;
std::shared_ptr<Data::DocumentMedia> _mainReactionMedia; std::shared_ptr<Data::DocumentMedia> _mainReactionMedia;
std::shared_ptr<Lottie::Icon> _mainReactionIcon;
QImage _mainReactionImage; QImage _mainReactionImage;
rpl::lifetime _mainReactionLifetime; rpl::lifetime _mainReactionLifetime;
rpl::variable<int> _uniqueLimit = 0;
base::flat_map<not_null<DocumentData*>, ReactionDocument> _loadCache; base::flat_map<not_null<DocumentData*>, ReactionDocument> _loadCache;
std::vector<ReactionIcons> _icons; std::vector<not_null<ReactionIcons*>> _icons;
rpl::lifetime _loadCacheLifetime; rpl::lifetime _loadCacheLifetime;
bool _showingAll = false; bool _showingAll = false;
mutable int _selectedIcon = -1; mutable int _selectedIcon = -1;
@ -297,9 +315,18 @@ private:
std::unique_ptr<Button> _button; std::unique_ptr<Button> _button;
std::vector<std::unique_ptr<Button>> _buttonHiding; std::vector<std::unique_ptr<Button>> _buttonHiding;
FullMsgId _buttonContext; FullMsgId _buttonContext;
base::flat_set<QString> _buttonAlreadyList;
int _buttonAlreadyNotMineCount = 0;
mutable base::flat_map<QString, ClickHandlerPtr> _reactionsLinks; mutable base::flat_map<QString, ClickHandlerPtr> _reactionsLinks;
Fn<Fn<void()>(QString)> _createChooseCallback; Fn<Fn<void()>(QString)> _createChooseCallback;
rpl::lifetime _lifetime;
}; };
void SetupManagerList(
not_null<Manager*> manager,
not_null<Main::Session*> session,
rpl::producer<std::optional<base::flat_set<QString>>> filter);
} // namespace HistoryView } // namespace HistoryView

View file

@ -1927,7 +1927,7 @@ CopyRestrictionType RepliesWidget::listSelectRestrictionType() {
} }
auto RepliesWidget::listAllowedReactionsValue() auto RepliesWidget::listAllowedReactionsValue()
-> rpl::producer<std::vector<Data::Reaction>> { -> rpl::producer<std::optional<base::flat_set<QString>>> {
return Data::PeerAllowedReactionsValue(_history->peer); return Data::PeerAllowedReactionsValue(_history->peer);
} }

View file

@ -142,7 +142,7 @@ public:
CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override; CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override;
CopyRestrictionType listSelectRestrictionType() override; CopyRestrictionType listSelectRestrictionType() override;
auto listAllowedReactionsValue() auto listAllowedReactionsValue()
-> rpl::producer<std::vector<Data::Reaction>> override; -> rpl::producer<std::optional<base::flat_set<QString>>> override;
protected: protected:
void resizeEvent(QResizeEvent *e) override; void resizeEvent(QResizeEvent *e) override;

View file

@ -1243,8 +1243,9 @@ CopyRestrictionType ScheduledWidget::listSelectRestrictionType() {
} }
auto ScheduledWidget::listAllowedReactionsValue() auto ScheduledWidget::listAllowedReactionsValue()
-> rpl::producer<std::vector<Data::Reaction>> { -> rpl::producer<std::optional<base::flat_set<QString>>> {
return rpl::single(std::vector<Data::Reaction>()); const auto empty = base::flat_set<QString>();
return rpl::single(std::optional<base::flat_set<QString>>(empty));
} }
void ScheduledWidget::confirmSendNowSelected() { void ScheduledWidget::confirmSendNowSelected() {

View file

@ -123,7 +123,7 @@ public:
CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override; CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override;
CopyRestrictionType listSelectRestrictionType() override; CopyRestrictionType listSelectRestrictionType() override;
auto listAllowedReactionsValue() auto listAllowedReactionsValue()
-> rpl::producer<std::vector<Data::Reaction>> override; -> rpl::producer<std::optional<base::flat_set<QString>>> override;
protected: protected:
void resizeEvent(QResizeEvent *e) override; void resizeEvent(QResizeEvent *e) override;