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_changes.h"
#include "data/data_message_reactions.h"
#include "data/data_peer_values.h"
#include "history/admin_log/history_admin_log_section.h"
#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
@ -1060,7 +1061,7 @@ void Controller::fillManageSection() {
!_peer->isBroadcast(),
session->data().reactions().list(
Data::Reactions::Type::Active),
session->data().reactions().list(_peer),
*Data::PeerAllowedReactions(_peer),
done));
},
st::infoIconReactions);

View file

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

View file

@ -19,7 +19,7 @@ void EditAllowedReactionsBox(
not_null<Ui::GenericBox*> box,
bool isGroup,
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);
void SaveAllowedReactions(

View file

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

View file

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

View file

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

View file

@ -11,11 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_changes.h"
#include "lottie/lottie_icon.h"
#include "base/timer_rpl.h"
#include "apiwrap.h"
@ -65,16 +63,6 @@ const std::vector<Reaction> &Reactions::list(Type type) const {
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 {
return _updated.events();
}
@ -214,33 +202,17 @@ void Reactions::downloadTaskFinished() {
}
}
std::vector<Reaction> Reactions::Filtered(
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(
base::flat_set<QString> Reactions::ParseAllowed(
const MTPVector<MTPstring> *list) {
if (!list) {
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);
}) | ranges::to_vector;
return { begin(parsed), end(parsed) };
}
void Reactions::request() {

View file

@ -43,15 +43,8 @@ public:
All,
};
[[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(
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(
[[nodiscard]] static base::flat_set<QString> ParseAllowed(
const MTPVector<MTPstring> *list);
[[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_message_reactions.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "ui/image/image_prepare.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) {
return rpl::combine(
rpl::single(
rpl::empty_value()
) | rpl::then(peer->owner().reactions().updates()),
peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::Reactions)
) | rpl::map([=] {
return peer->owner().reactions().list(peer);
if (const auto chat = peer->asChat()) {
return chat->allowedReactions();
} else if (const auto channel = peer->asChannel()) {
return channel->allowedReactions();
} else {
return std::nullopt;
}
}
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

View file

@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
enum class ImageRoundRadius;
namespace Main {
class Session;
} // namespace Main
namespace Data {
struct Reaction;
@ -122,7 +126,12 @@ inline auto PeerFullFlagValue(
int size,
ImageRoundRadius radius);
[[nodiscard]] std::optional<base::flat_set<QString>> PeerAllowedReactions(
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

View file

@ -322,6 +322,7 @@ HistoryInner::HistoryInner(
, _reactionsManager(
std::make_unique<HistoryView::Reactions::Manager>(
this,
Data::UniqueReactionsLimitValue(&controller->session()),
[=](QRect updated) { update(updated); }))
, _touchSelectTimer([=] { onTouchSelect(); })
, _touchScrollTimer([=] { onTouchScrollTimer(); })
@ -412,6 +413,7 @@ HistoryInner::HistoryInner(
return item->mainView() != nullptr;
}) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
item->mainView()->itemDataChanged();
_reactionsManager->updateUniqueLimit(item);
}, lifetime());
session().changes().historyUpdates(
@ -421,11 +423,10 @@ HistoryInner::HistoryInner(
update();
}, lifetime());
Data::PeerAllowedReactionsValue(
_peer
) | rpl::start_with_next([=](std::vector<Data::Reaction> &&list) {
_reactionsManager->applyList(std::move(list));
}, lifetime());
HistoryView::Reactions::SetupManagerList(
_reactionsManager.get(),
&session(),
Data::PeerAllowedReactionsValue(_peer));
controller->adaptive().chatWideValue(
) | rpl::start_with_next([=](bool wide) {
@ -3169,7 +3170,8 @@ void HistoryInner::mouseActionUpdate() {
: nullptr;
const auto item = view ? view->data().get() : nullptr;
if (view) {
if (App::mousedItem() != view) {
const auto changed = (App::mousedItem() != view);
if (changed) {
repaintItem(App::mousedItem());
App::mousedItem(view);
repaintItem(App::mousedItem());
@ -3179,6 +3181,9 @@ void HistoryInner::mouseActionUpdate() {
view,
m,
reactionState));
if (changed) {
_reactionsManager->updateUniqueLimit(item);
}
if (view->pointState(m) != PointState::Outside) {
if (App::hoveredItem() != view) {
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_file_click_handler.h"
#include "data/data_message_reactions.h"
#include "data/data_peer_values.h"
#include "facades.h"
#include "styles/style_chat.h"
@ -267,6 +268,7 @@ ListWidget::ListWidget(
, _reactionsManager(
std::make_unique<Reactions::Manager>(
this,
Data::UniqueReactionsLimitValue(&controller->session()),
[=](QRect updated) { update(updated); }))
, _scrollDateCheck([this] { scrollDateCheck(); })
, _applyUpdatedScrollState([this] { applyUpdatedScrollState(); })
@ -344,10 +346,10 @@ ListWidget::ListWidget(
}
}, lifetime());
_delegate->listAllowedReactionsValue(
) | rpl::start_with_next([=](std::vector<Data::Reaction> &&list) {
_reactionsManager->applyList(std::move(list));
}, lifetime());
Reactions::SetupManagerList(
_reactionsManager.get(),
&session(),
_delegate->listAllowedReactionsValue());
controller->adaptive().chatWideValue(
) | rpl::start_with_next([=](bool wide) {
@ -2563,7 +2565,8 @@ void ListWidget::mouseActionUpdate() {
view ? view->height() : 0,
itemPoint,
view ? view->pointState(itemPoint) : PointState::Outside);
if (_overElement != view) {
const auto viewChanged = (_overElement != view);
if (viewChanged) {
repaintItem(_overElement);
_overElement = view;
repaintItem(_overElement);
@ -2574,6 +2577,9 @@ void ListWidget::mouseActionUpdate() {
itemPoint,
reactionState)
: Reactions::ButtonParameters());
if (viewChanged && view) {
_reactionsManager->updateUniqueLimit(item);
}
TextState dragState;
ClickHandlerHost *lnkhost = nullptr;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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