Implement PoC custom reaction selection.

This commit is contained in:
John Preston 2022-08-16 18:52:49 +03:00
parent cece9cf09b
commit 09124f6424
24 changed files with 449 additions and 152 deletions

View file

@ -1015,7 +1015,7 @@ void Controller::fillManageSection() {
!_peer->isBroadcast(),
session->data().reactions().list(
Data::Reactions::Type::Active),
*Data::PeerAllowedReactions(_peer),
*Data::PeerReactionsFilter(_peer).allowed,
done));
},
{ &st::infoRoundedIconReactions, Settings::kIconRed });

View file

@ -566,45 +566,50 @@ void TabbedSelector::resizeEvent(QResizeEvent *e) {
_tabsSlider->width(),
st::lineWidth);
}
updateScrollGeometry(e->oldSize());
updateRestrictedLabelGeometry();
updateFooterGeometry();
update();
}
void TabbedSelector::updateScrollGeometry(QSize oldSize) {
auto scrollWidth = width() - st::roundRadiusSmall;
auto scrollHeight = height() - scrollTop() - scrollBottom();
auto inner = currentTab()->widget();
auto innerWidth = scrollWidth - st::emojiScroll.width;
auto updateScrollGeometry = [&] {
auto setScrollGeometry = [&] {
_scroll->setGeometryToLeft(
st::roundRadiusSmall,
scrollTop(),
scrollWidth,
scrollHeight);
};
auto updateInnerGeometry = [&] {
auto setInnerGeometry = [&] {
auto scrollTop = _scroll->scrollTop();
auto scrollBottom = scrollTop + scrollHeight;
inner->setMinimalHeight(innerWidth, scrollHeight);
inner->setVisibleTopBottom(scrollTop, scrollBottom);
};
if (e->oldSize().height() > height()) {
updateScrollGeometry();
updateInnerGeometry();
if (oldSize.height() > height()) {
setScrollGeometry();
setInnerGeometry();
} else {
updateInnerGeometry();
updateScrollGeometry();
setInnerGeometry();
setScrollGeometry();
}
_bottomShadow->setGeometry(
0,
_scroll->y() + _scroll->height() - st::lineWidth,
width(),
st::lineWidth);
updateRestrictedLabelGeometry();
}
void TabbedSelector::updateFooterGeometry() {
_footerTop = _dropDown ? 0 : (height() - st::emojiFooterHeight);
for (auto &tab : _tabs) {
tab.footer()->resizeToWidth(width());
tab.footer()->moveToLeft(0, _footerTop);
}
update();
}
void TabbedSelector::updateRestrictedLabelGeometry() {
@ -1139,6 +1144,15 @@ void TabbedSelector::showMenuWithType(SendMenu::Type type) {
}
}
void TabbedSelector::setDropDown(bool dropDown) {
if (_dropDown == dropDown) {
return;
}
_dropDown = dropDown;
updateFooterGeometry();
updateScrollGeometry(size());
}
rpl::producer<> TabbedSelector::contextMenuRequested() const {
return events(
) | rpl::filter([=](not_null<QEvent*> e) {

View file

@ -130,9 +130,7 @@ public:
}
void showMenuWithType(SendMenu::Type type);
void setDropDown(bool dropDown) {
_dropDown = dropDown;
}
void setDropDown(bool dropDown);
// Float player interface.
bool floatPlayerHandleWheelEvent(QEvent *e);
@ -204,6 +202,8 @@ private:
void checkRestrictedPeer();
bool isRestrictedView();
void updateRestrictedLabelGeometry();
void updateScrollGeometry(QSize oldSize);
void updateFooterGeometry();
void handleScroll();
QImage grabForAnimation();

View file

@ -0,0 +1,32 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_message_reaction_id.h"
namespace Data {
ReactionId ReactionFromMTP(const MTPReaction &reaction) {
return reaction.match([](MTPDreactionEmpty) {
return ReactionId{ QString() };
}, [](const MTPDreactionEmoji &data) {
return ReactionId{ qs(data.vemoticon()) };
}, [](const MTPDreactionCustomEmoji &data) {
return ReactionId{ DocumentId(data.vdocument_id().v) };
});
}
MTPReaction ReactionToMTP(ReactionId id) {
if (const auto custom = id.custom()) {
return MTP_reactionCustomEmoji(MTP_long(custom));
}
const auto emoji = id.emoji();
return emoji.isEmpty()
? MTP_reactionEmpty()
: MTP_reactionEmoji(MTP_string(emoji));
}
} // namespace Data

View file

@ -0,0 +1,49 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Data {
struct ReactionId {
std::variant<QString, DocumentId> data;
[[nodiscard]] bool empty() const {
const auto emoji = std::get_if<QString>(&data);
return emoji && emoji->isEmpty();
}
[[nodiscard]] QString emoji() const {
const auto emoji = std::get_if<QString>(&data);
return emoji ? *emoji : QString();
}
[[nodiscard]] DocumentId custom() const {
const auto custom = std::get_if<DocumentId>(&data);
return custom ? *custom : DocumentId();
}
};
Q_DECLARE_METATYPE(ReactionId);
inline bool operator<(const ReactionId &a, const ReactionId &b) {
return a.data < b.data;
}
inline bool operator==(const ReactionId &a, const ReactionId &b) {
return a.data == b.data;
}
[[nodiscard]] ReactionId ReactionFromMTP(const MTPReaction &reaction);
[[nodiscard]] MTPReaction ReactionToMTP(ReactionId id);
struct ReactionsFilter {
std::optional<base::flat_set<QString>> allowed;
bool customAllowed = false;
friend inline auto operator<=>(
const ReactionsFilter &,
const ReactionsFilter &) = default;
};
} // namespace Data

View file

@ -42,26 +42,6 @@ constexpr auto kSizeForDownscale = 64;
} // namespace
ReactionId ReactionFromMTP(const MTPReaction &reaction) {
return reaction.match([](MTPDreactionEmpty) {
return ReactionId{ QString() };
}, [](const MTPDreactionEmoji &data) {
return ReactionId{ qs(data.vemoticon()) };
}, [](const MTPDreactionCustomEmoji &data) {
return ReactionId{ DocumentId(data.vdocument_id().v) };
});
}
MTPReaction ReactionToMTP(ReactionId id) {
if (const auto custom = id.custom()) {
return MTP_reactionCustomEmoji(MTP_long(custom));
}
const auto emoji = id.emoji();
return emoji.isEmpty()
? MTP_reactionEmpty()
: MTP_reactionEmoji(MTP_string(emoji));
}
Reactions::Reactions(not_null<Session*> owner)
: _owner(owner)
, _repaintTimer([=] { repaintCollected(); }) {

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/timer.h"
#include "data/data_message_reaction_id.h"
namespace Lottie {
class Icon;
@ -18,34 +19,6 @@ namespace Data {
class DocumentMedia;
class Session;
struct ReactionId {
std::variant<QString, DocumentId> data;
[[nodiscard]] bool empty() const {
const auto emoji = std::get_if<QString>(&data);
return emoji && emoji->isEmpty();
}
[[nodiscard]] QString emoji() const {
const auto emoji = std::get_if<QString>(&data);
return emoji ? *emoji : QString();
}
[[nodiscard]] DocumentId custom() const {
const auto custom = std::get_if<DocumentId>(&data);
return custom ? *custom : DocumentId();
}
};
Q_DECLARE_METATYPE(ReactionId);
inline bool operator<(const ReactionId &a, const ReactionId &b) {
return a.data < b.data;
}
inline bool operator==(const ReactionId &a, const ReactionId &b) {
return a.data == b.data;
}
[[nodiscard]] ReactionId ReactionFromMTP(const MTPReaction &reaction);
[[nodiscard]] MTPReaction ReactionToMTP(ReactionId id);
struct Reaction {
ReactionId id;
QString title;

View file

@ -513,25 +513,23 @@ rpl::producer<QImage> PeerUserpicImageValue(
};
}
std::optional<base::flat_set<QString>> PeerAllowedReactions(
not_null<PeerData*> peer) {
ReactionsFilter PeerReactionsFilter(not_null<PeerData*> peer) {
if (const auto chat = peer->asChat()) {
return chat->allowedReactions();
return { .allowed = chat->allowedReactions() };
} else if (const auto channel = peer->asChannel()) {
return channel->allowedReactions();
return { .allowed = channel->allowedReactions() };
} else {
return std::nullopt;
return { .customAllowed = true };
}
}
auto PeerAllowedReactionsValue(
not_null<PeerData*> peer)
-> rpl::producer<std::optional<base::flat_set<QString>>> {
rpl::producer<ReactionsFilter> PeerReactionsFilterValue(
not_null<PeerData*> peer) {
return peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::Reactions
) | rpl::map([=]{
return PeerAllowedReactions(peer);
return PeerReactionsFilter(peer);
});
}

View file

@ -21,6 +21,7 @@ class Session;
namespace Data {
struct Reaction;
struct ReactionsFilter;
template <typename ChangeType, typename Error, typename Generator>
inline auto FlagsValueWithMask(
@ -133,10 +134,9 @@ inline auto PeerFullFlagValue(
int size,
ImageRoundRadius radius);
[[nodiscard]] std::optional<base::flat_set<QString>> PeerAllowedReactions(
[[nodiscard]] ReactionsFilter PeerReactionsFilter(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<ReactionsFilter> PeerReactionsFilterValue(
not_null<PeerData*> peer);
[[nodiscard]] auto PeerAllowedReactionsValue(not_null<PeerData*> peer)
-> rpl::producer<std::optional<base::flat_set<QString>>>;
[[nodiscard]] rpl::producer<int> UniqueReactionsLimitValue(
not_null<Main::Session*> session);

View file

@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_context_menu.h"
#include "history/view/history_view_quick_action.h"
#include "history/view/history_view_react_button.h"
#include "history/view/history_view_react_selector.h"
#include "history/view/history_view_emoji_interactions.h"
#include "history/history_item_components.h"
#include "history/history_item_text.h"
@ -345,6 +346,7 @@ HistoryInner::HistoryInner(
Data::UniqueReactionsLimitValue(&controller->session()),
[=](QRect updated) { update(updated); },
controller->cachedReactionIconFactory().createMethod()))
, _reactionsSelector(std::make_unique<HistoryView::Reactions::Selector>())
, _touchSelectTimer([=] { onTouchSelect(); })
, _touchScrollTimer([=] { onTouchScrollTimer(); })
, _scrollDateCheck([this] { scrollDateCheck(); })
@ -393,28 +395,25 @@ HistoryInner::HistoryInner(
_controller->emojiInteractions().playStarted(_peer, std::move(emoji));
}, lifetime());
using ChosenReaction = HistoryView::Reactions::Manager::Chosen;
_reactionsManager->chosen(
rpl::merge(
_reactionsManager->chosen(),
_reactionsSelector->chosen()
) | rpl::start_with_next([=](ChosenReaction reaction) {
const auto item = session().data().message(reaction.context);
if (!item
|| Window::ShowReactPremiumError(
_reactionsManager->updateButton({});
reactionChosen(reaction);
}, lifetime());
_reactionsManager->setExternalSelectorShown(_reactionsSelector->shown());
_reactionsManager->expandSelectorRequests(
) | rpl::start_with_next([=](ReactionExpandRequest request) {
if (request.expanded) {
_reactionsSelector->show(
_controller,
item,
reaction.id)) {
return;
}
item->toggleReaction(reaction.id);
if (item->chosenReaction() != reaction.id) {
return;
} else if (const auto view = item->mainView()) {
if (const auto top = itemTop(view); top >= 0) {
view->animateReaction({
.id = reaction.id,
.flyIcon = reaction.icon,
.flyFrom = reaction.geometry.translated(0, -top),
});
}
this,
request.context,
request.button);
} else {
_reactionsSelector->hide();
}
}, lifetime());
@ -463,7 +462,7 @@ HistoryInner::HistoryInner(
HistoryView::Reactions::SetupManagerList(
_reactionsManager.get(),
&session(),
Data::PeerAllowedReactionsValue(_peer));
Data::PeerReactionsFilterValue(_peer));
controller->adaptive().chatWideValue(
) | rpl::start_with_next([=](bool wide) {
@ -478,6 +477,31 @@ HistoryInner::HistoryInner(
setupSharingDisallowed();
}
void HistoryInner::reactionChosen(const ChosenReaction &reaction) {
const auto guard = gsl::finally([&] { _reactionsSelector->hide(); });
const auto item = session().data().message(reaction.context);
if (!item
|| Window::ShowReactPremiumError(
_controller,
item,
reaction.id)) {
return;
}
item->toggleReaction(reaction.id);
if (item->chosenReaction() != reaction.id) {
return;
} else if (const auto view = item->mainView()) {
if (const auto top = itemTop(view); top >= 0) {
view->animateReaction({
.id = reaction.id,
.flyIcon = reaction.icon,
.flyFrom = reaction.geometry.translated(0, -top),
});
}
}
}
Main::Session &HistoryInner::session() const {
return _controller->session();
}
@ -1925,10 +1949,11 @@ void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) {
void HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const {
const auto favorite = session().data().reactions().favorite();
const auto allowed = _reactionsManager->allowedSublist();
if (allowed
&& (favorite.emoji().isEmpty()
|| !allowed->contains(favorite.emoji()))) {
const auto &filter = _reactionsManager->filter();
if (favorite.emoji().isEmpty() && !filter.customAllowed) {
return;
} else if (filter.allowed
&& !filter.allowed->contains(favorite.emoji())) {
return;
}
const auto item = view->data();

View file

@ -36,6 +36,9 @@ class Element;
namespace HistoryView::Reactions {
class Manager;
class Selector;
struct ChosenReaction;
struct ExpandRequest;
struct ButtonParameters;
} // namespace HistoryView::Reactions
@ -225,6 +228,8 @@ private:
void onTouchScrollTimer();
class BotAbout;
using ChosenReaction = HistoryView::Reactions::ChosenReaction;
using ReactionExpandRequest = HistoryView::Reactions::ExpandRequest;
using VideoUserpic = Dialogs::Ui::VideoUserpic;
using SelectedItems = std::map<HistoryItem*, TextSelection, std::less<>>;
enum class MouseAction {
@ -396,6 +401,7 @@ private:
const HistoryView::TextState &reactionState) const
-> HistoryView::Reactions::ButtonParameters;
void toggleFavoriteReaction(not_null<Element*> view) const;
void reactionChosen(const ChosenReaction &reaction);
void setupSharingDisallowed();
[[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const;
@ -458,6 +464,7 @@ private:
std::unique_ptr<VideoUserpic>> _videoUserpics;
std::unique_ptr<HistoryView::Reactions::Manager> _reactionsManager;
std::unique_ptr<HistoryView::Reactions::Selector> _reactionsSelector;
MouseAction _mouseAction = MouseAction::None;
TextSelectType _mouseSelectType = TextSelectType::Letters;

View file

@ -353,9 +353,10 @@ ListWidget::ListWidget(
}
}, lifetime());
using ChosenReaction = Reactions::Manager::Chosen;
_reactionsManager->chosen(
) | rpl::start_with_next([=](ChosenReaction reaction) {
_reactionsManager->updateButton({});
const auto item = session().data().message(reaction.context);
if (!item
|| Window::ShowReactPremiumError(
@ -2115,10 +2116,11 @@ void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {
void ListWidget::toggleFavoriteReaction(not_null<Element*> view) const {
const auto favorite = session().data().reactions().favorite();
const auto allowed = _reactionsManager->allowedSublist();
if (allowed
&& (favorite.emoji().isEmpty()
|| !allowed->contains(favorite.emoji()))) {
const auto &filter = _reactionsManager->filter();
if (favorite.emoji().isEmpty() && !filter.customAllowed) {
return;
} else if (filter.allowed
&& !filter.allowed->contains(favorite.emoji())) {
return;
}
const auto item = view->data();

View file

@ -36,10 +36,12 @@ namespace Data {
struct Group;
class CloudImageView;
struct Reaction;
struct ReactionsFilter;
} // namespace Data
namespace HistoryView::Reactions {
class Manager;
struct ChosenReaction;
struct ButtonParameters;
} // namespace HistoryView::Reactions
@ -118,7 +120,7 @@ public:
}
virtual CopyRestrictionType listSelectRestrictionType() = 0;
virtual auto listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> = 0;
-> rpl::producer<Data::ReactionsFilter> = 0;
virtual void listShowPremiumToast(not_null<DocumentData*> document) = 0;
};
@ -379,6 +381,7 @@ private:
using ScrollTopState = ListMemento::ScrollTopState;
using PointState = HistoryView::PointState;
using CursorState = HistoryView::CursorState;
using ChosenReaction = HistoryView::Reactions::ChosenReaction;
void refreshViewer();
void updateAroundPositionFromNearest(int nearestIndex);

View file

@ -684,8 +684,8 @@ CopyRestrictionType PinnedWidget::listSelectRestrictionType() {
}
auto PinnedWidget::listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> {
return Data::PeerAllowedReactionsValue(_history->peer);
-> rpl::producer<Data::ReactionsFilter> {
return Data::PeerReactionsFilterValue(_history->peer);
}
void PinnedWidget::listShowPremiumToast(not_null<DocumentData*> document) {

View file

@ -106,7 +106,7 @@ public:
CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override;
CopyRestrictionType listSelectRestrictionType() override;
auto listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> override;
-> rpl::producer<Data::ReactionsFilter> override;
void listShowPremiumToast(not_null<DocumentData*> document) override;
protected:

View file

@ -95,18 +95,24 @@ constexpr auto kMaxReactionsScrollAtOnce = 2;
Button::Button(
Fn<void(QRect)> update,
ButtonParameters parameters,
Fn<void()> hideMe)
Fn<void(bool expanded)> toggleExpanded,
Fn<void()> hide)
: _update(std::move(update))
, _toggleExpanded(std::move(toggleExpanded))
, _finalScale(ScaleForState(_state))
, _collapsed(QPoint(), CountOuterSize())
, _finalHeight(_collapsed.height())
, _expandTimer([=] { applyState(State::Inside, _update); })
, _hideTimer(hideMe) {
, _expandTimer([=] { _toggleExpanded(true); })
, _hideTimer(hide) {
applyParameters(parameters, nullptr);
}
Button::~Button() = default;
void Button::expandWithoutCustom() {
applyState(State::Inside, _update);
}
bool Button::isHidden() const {
return (_state == State::Hidden) && !_opacityAnimation.animating();
}
@ -316,6 +322,7 @@ void Button::applyState(State state, Fn<void(QRect)> update) {
_finalScale = finalScale;
}
_state = state;
_toggleExpanded(false);
}
float64 Button::ScaleForState(State state) {
@ -410,15 +417,14 @@ Manager::Manager(
_createChooseCallback = [=](ReactionId id) {
return [=] {
if (auto chosen = lookupChosen(id)) {
updateButton({});
_chosen.fire(std::move(chosen));
}
};
};
}
Manager::Chosen Manager::lookupChosen(const ReactionId &id) const {
auto result = Chosen{
ChosenReaction Manager::lookupChosen(const ReactionId &id) const {
auto result = ChosenReaction{
.context = _buttonContext,
.id = id,
};
@ -461,21 +467,26 @@ Manager::Chosen Manager::lookupChosen(const ReactionId &id) const {
return result;
}
void Manager::applyListFilters() {
bool Manager::applyUniqueLimit() const {
const auto limit = _uniqueLimit.current();
const auto applyUniqueLimit = _buttonContext
return _buttonContext
&& (limit > 0)
&& (_buttonAlreadyNotMineCount >= limit);
}
void Manager::applyListFilters() {
const auto limited = applyUniqueLimit();
auto icons = std::vector<not_null<ReactionIcons*>>();
icons.reserve(_list.size());
auto showPremiumLock = (ReactionIcons*)nullptr;
auto favoriteIndex = -1;
for (auto &icon : _list) {
const auto &id = icon.id;
const auto add = applyUniqueLimit
const auto add = limited
? _buttonAlreadyList.contains(id)
: (!_filter
|| (!id.emoji().isEmpty() && _filter->contains(id.emoji())));
: id.emoji().isEmpty()
? _filter.customAllowed
: (!_filter.allowed || _filter.allowed->contains(id.emoji()));
if (add) {
if (icon.premium
&& !_allowSendingPremium
@ -504,6 +515,9 @@ void Manager::applyListFilters() {
const auto first = begin(icons);
std::rotate(first, first + favoriteIndex, first + favoriteIndex + 1);
}
if (!limited && _filter.customAllowed && icons.size() > 1) {
icons.erase(begin(icons) + 1, end(icons));
}
if (_icons == icons) {
return;
}
@ -528,8 +542,13 @@ void Manager::stealWheelEvents(not_null<QWidget*> target) {
Manager::~Manager() = default;
void Manager::updateButton(ButtonParameters parameters) {
if (parameters.cursorLeft && _menu) {
return;
if (parameters.cursorLeft) {
if (_menu) {
return;
} else if (_externalSelectorShown) {
setSelectedIcon(-1);
return;
}
}
const auto contextChanged = (_buttonContext != parameters.context);
if (contextChanged) {
@ -537,6 +556,7 @@ void Manager::updateButton(ButtonParameters parameters) {
if (_button) {
_button->applyState(ButtonState::Hidden);
_buttonHiding.push_back(std::move(_button));
_expandSelectorRequests.fire({ .expanded = false });
}
_buttonShowTimer.cancel();
_scheduledParameters = std::nullopt;
@ -567,11 +587,32 @@ void Manager::updateButton(ButtonParameters parameters) {
}
}
void Manager::toggleExpanded(bool expanded) {
if (!_button || !_buttonContext) {
} else if (!expanded || (_filter.customAllowed && !applyUniqueLimit())) {
_expandSelectorRequests.fire({
.context = _buttonContext,
.button = _button->geometry().marginsRemoved(
st::reactionCornerShadow),
.expanded = expanded,
});
} else {
_button->expandWithoutCustom();
}
}
void Manager::setExternalSelectorShown(rpl::producer<bool> shown) {
std::move(shown) | rpl::start_with_next([=](bool shown) {
_externalSelectorShown = shown;
}, _lifetime);
}
void Manager::showButtonDelayed() {
clearAppearAnimations();
_button = std::make_unique<Button>(
_buttonUpdate,
*_scheduledParameters,
[=](bool expanded) { toggleExpanded(expanded); },
[=]{ updateButton({}); });
}
@ -614,7 +655,7 @@ void Manager::applyList(
setSelectedIcon((selected < _icons.size()) ? selected : -1);
}
void Manager::updateAllowedSublist(AllowedSublist filter) {
void Manager::updateFilter(Data::ReactionsFilter filter) {
if (_filter == filter) {
return;
}
@ -630,7 +671,7 @@ void Manager::updateAllowSendingPremium(bool allow) {
applyListFilters();
}
const Manager::AllowedSublist &Manager::allowedSublist() const {
const Data::ReactionsFilter &Manager::filter() const {
return _filter;
}
@ -1662,7 +1703,7 @@ auto Manager::faveRequests() const -> rpl::producer<ReactionId> {
void SetupManagerList(
not_null<Manager*> manager,
not_null<Main::Session*> session,
rpl::producer<Manager::AllowedSublist> filter) {
rpl::producer<Data::ReactionsFilter> filter) {
const auto reactions = &session->data().reactions();
rpl::single(rpl::empty) | rpl::then(
reactions->updates()
@ -1675,8 +1716,8 @@ void SetupManagerList(
std::move(
filter
) | rpl::start_with_next([=](Manager::AllowedSublist &&list) {
manager->updateAllowedSublist(std::move(list));
) | rpl::start_with_next([=](Data::ReactionsFilter &&list) {
manager->updateFilter(std::move(list));
}, manager->lifetime());
manager->faveRequests(

View file

@ -73,10 +73,12 @@ public:
Button(
Fn<void(QRect)> update,
ButtonParameters parameters,
Fn<void()> hideMe);
Fn<void(bool)> toggleExpanded,
Fn<void()> hide);
~Button();
void applyParameters(ButtonParameters parameters);
void expandWithoutCustom();
using State = ButtonState;
void applyState(State state);
@ -110,6 +112,8 @@ private:
void updateExpandDirection(const ButtonParameters &parameters);
const Fn<void(QRect)> _update;
const Fn<void(bool)> _toggleExpanded;
State _state = State::Hidden;
float64 _finalScale = 0.;
Ui::Animations::Simple _scaleAnimation;
@ -131,6 +135,23 @@ private:
};
struct ChosenReaction {
FullMsgId context;
Data::ReactionId id;
std::shared_ptr<Lottie::Icon> icon;
QRect geometry;
explicit operator bool() const {
return context && !id.empty();
}
};
struct ExpandRequest {
FullMsgId context;
QRect button;
bool expanded = false;
};
using IconFactory = Fn<std::shared_ptr<Lottie::Icon>(
not_null<Data::DocumentMedia*>,
int)>;
@ -145,15 +166,14 @@ public:
~Manager();
using ReactionId = ::Data::ReactionId;
using AllowedSublist = std::optional<base::flat_set<QString>>;
void applyList(
const std::vector<Data::Reaction> &list,
const ReactionId &favorite,
bool premiumPossible);
void updateAllowedSublist(AllowedSublist filter);
void updateFilter(Data::ReactionsFilter filter);
void updateAllowSendingPremium(bool allow);
[[nodiscard]] const AllowedSublist &allowedSublist() const;
[[nodiscard]] const Data::ReactionsFilter &filter() const;
void updateUniqueLimit(not_null<HistoryItem*> item);
void updateButton(ButtonParameters parameters);
@ -163,19 +183,14 @@ public:
[[nodiscard]] bool consumeWheelEvent(not_null<QWheelEvent*> e);
struct Chosen {
FullMsgId context;
ReactionId id;
std::shared_ptr<Lottie::Icon> icon;
QRect geometry;
explicit operator bool() const {
return context && !id.empty();
}
};
[[nodiscard]] rpl::producer<Chosen> chosen() const {
[[nodiscard]] rpl::producer<ChosenReaction> chosen() const {
return _chosen.events();
}
[[nodiscard]] auto expandSelectorRequests() const
-> rpl::producer<ExpandRequest> {
return _expandSelectorRequests.events();
}
void setExternalSelectorShown(rpl::producer<bool> shown);
[[nodiscard]] std::optional<QRect> lookupEffectArea(
FullMsgId itemId) const;
@ -223,8 +238,10 @@ private:
void showButtonDelayed();
void stealWheelEvents(not_null<QWidget*> target);
[[nodiscard]] Chosen lookupChosen(const ReactionId &id) const;
[[nodiscard]] ChosenReaction lookupChosen(const ReactionId &id) const;
[[nodiscard]] bool overCurrentButton(QPoint position) const;
[[nodiscard]] bool applyUniqueLimit() const;
void toggleExpanded(bool expanded);
void removeStaleButtons();
void paintButton(
@ -314,10 +331,11 @@ private:
void checkIcons();
const IconFactory _iconFactory;
rpl::event_stream<Chosen> _chosen;
rpl::event_stream<ChosenReaction> _chosen;
rpl::event_stream<ExpandRequest> _expandSelectorRequests;
std::vector<ReactionIcons> _list;
ReactionId _favorite;
AllowedSublist _filter;
Data::ReactionsFilter _filter;
QSize _outer;
QRect _inner;
QSize _overlayFull;
@ -372,6 +390,7 @@ private:
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<ReactionId> _faveRequests;
bool _externalSelectorShown = false;
rpl::lifetime _lifetime;
@ -395,7 +414,7 @@ private:
void SetupManagerList(
not_null<Manager*> manager,
not_null<Main::Session*> session,
rpl::producer<Manager::AllowedSublist> filter);
rpl::producer<Data::ReactionsFilter> filter);
[[nodiscard]] std::shared_ptr<Lottie::Icon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,

View file

@ -0,0 +1,107 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/history_view_react_selector.h"
#include "history/view/history_view_react_button.h"
#include "data/data_document.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "mainwindow.h"
#include "styles/style_chat_helpers.h"
namespace HistoryView::Reactions {
void Selector::show(
not_null<Window::SessionController*> controller,
not_null<QWidget*> widget,
FullMsgId contextId,
QRect around) {
if (!_panel) {
create(controller);
} else if (_contextId == contextId
&& (!_panel->hiding() && !_panel->isHidden())) {
return;
}
_contextId = contextId;
const auto parent = _panel->parentWidget();
const auto global = widget->mapToGlobal(around.topLeft());
const auto local = parent->mapFromGlobal(global);
const auto availableTop = local.y();
const auto availableBottom = parent->height()
- local.y()
- around.height();
if (availableTop >= st::emojiPanMinHeight
|| availableTop >= availableBottom) {
_panel->setDropDown(false);
_panel->moveBottomRight(
local.y(),
local.x() + around.width() * 3);
} else {
_panel->setDropDown(true);
_panel->moveTopRight(
local.y() + around.height(),
local.x() + around.width() * 3);
}
_panel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
_panel->showAnimated();
}
rpl::producer<ChosenReaction> Selector::chosen() const {
return _chosen.events();
}
rpl::producer<bool> Selector::shown() const {
return _shown.events();
}
void Selector::create(
not_null<Window::SessionController*> controller) {
using Selector = ChatHelpers::TabbedSelector;
_panel = base::make_unique_q<ChatHelpers::TabbedPanel>(
controller->window().widget()->bodyWidget(),
controller,
object_ptr<Selector>(
nullptr,
controller,
Window::GifPauseReason::Layer,
ChatHelpers::TabbedSelector::Mode::EmojiStatus));
_panel->shownValue() | rpl::start_to_stream(_shown, _panel->lifetime());
_panel->hide();
_panel->selector()->setAllowEmojiWithoutPremium(false);
auto statusChosen = _panel->selector()->customEmojiChosen(
) | rpl::map([=](Selector::FileChosen data) {
return data.document->id;
});
rpl::merge(
std::move(statusChosen),
_panel->selector()->emojiChosen() | rpl::map_to(DocumentId())
) | rpl::start_with_next([=](DocumentId id) {
_chosen.fire(ChosenReaction{ .context = _contextId, .id = { id } });
}, _panel->lifetime());
_panel->selector()->showPromoForPremiumEmoji();
}
void Selector::hide(anim::type animated) {
if (!_panel || _panel->isHidden()) {
return;
} else if (animated == anim::type::instant) {
_panel->hideFast();
} else {
_panel->hideAnimated();
}
}
} // namespace HistoryView::Reactions

View file

@ -0,0 +1,47 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "ui/effects/animation_value.h"
namespace ChatHelpers {
class TabbedPanel;
} // namespace ChatHelpers
namespace Window {
class SessionController;
} // namespace Window
namespace HistoryView::Reactions {
struct ChosenReaction;
class Selector final {
public:
void show(
not_null<Window::SessionController*> controller,
not_null<QWidget*> widget,
FullMsgId contextId,
QRect around);
void hide(anim::type animated = anim::type::normal);
[[nodiscard]] rpl::producer<ChosenReaction> chosen() const;
[[nodiscard]] rpl::producer<bool> shown() const;
private:
void create(not_null<Window::SessionController*> controller);
rpl::event_stream<bool> _shown;
base::unique_qptr<ChatHelpers::TabbedPanel> _panel;
rpl::event_stream<ChosenReaction> _chosen;
FullMsgId _contextId;
};
} // namespace HistoryView::Reactions

View file

@ -2041,8 +2041,8 @@ CopyRestrictionType RepliesWidget::listSelectRestrictionType() {
}
auto RepliesWidget::listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> {
return Data::PeerAllowedReactionsValue(_history->peer);
-> rpl::producer<Data::ReactionsFilter> {
return Data::PeerReactionsFilterValue(_history->peer);
}
void RepliesWidget::listShowPremiumToast(not_null<DocumentData*> document) {

View file

@ -143,7 +143,7 @@ public:
CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override;
CopyRestrictionType listSelectRestrictionType() override;
auto listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> override;
->rpl::producer<Data::ReactionsFilter> override;
void listShowPremiumToast(not_null<DocumentData*> document) override;
protected:

View file

@ -1355,9 +1355,9 @@ CopyRestrictionType ScheduledWidget::listSelectRestrictionType() {
}
auto ScheduledWidget::listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> {
-> rpl::producer<Data::ReactionsFilter> {
const auto empty = base::flat_set<QString>();
return rpl::single(std::optional<base::flat_set<QString>>(empty));
return rpl::single(Data::ReactionsFilter{ .allowed = empty });
}
void ScheduledWidget::listShowPremiumToast(

View file

@ -128,7 +128,7 @@ public:
CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override;
CopyRestrictionType listSelectRestrictionType() override;
auto listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> override;
-> rpl::producer<Data::ReactionsFilter> override;
void listShowPremiumToast(not_null<DocumentData*> document) override;
protected:

View file

@ -351,7 +351,7 @@ bool ShowSendPremiumError(
const auto type = peer->isBroadcast()
? ReactionDisableType::Channel
: ReactionDisableType::Group;
if (const auto allowed = Data::PeerAllowedReactions(peer)) {
if (const auto allowed = Data::PeerReactionsFilter(peer).allowed) {
for (const auto &reaction : list) {
if (reaction.premium
&& !allowed->contains(reaction.id.emoji())) {