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(), !_peer->isBroadcast(),
session->data().reactions().list( session->data().reactions().list(
Data::Reactions::Type::Active), Data::Reactions::Type::Active),
*Data::PeerAllowedReactions(_peer), *Data::PeerReactionsFilter(_peer).allowed,
done)); done));
}, },
{ &st::infoRoundedIconReactions, Settings::kIconRed }); { &st::infoRoundedIconReactions, Settings::kIconRed });

View file

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

View file

@ -130,9 +130,7 @@ public:
} }
void showMenuWithType(SendMenu::Type type); void showMenuWithType(SendMenu::Type type);
void setDropDown(bool dropDown) { void setDropDown(bool dropDown);
_dropDown = dropDown;
}
// Float player interface. // Float player interface.
bool floatPlayerHandleWheelEvent(QEvent *e); bool floatPlayerHandleWheelEvent(QEvent *e);
@ -204,6 +202,8 @@ private:
void checkRestrictedPeer(); void checkRestrictedPeer();
bool isRestrictedView(); bool isRestrictedView();
void updateRestrictedLabelGeometry(); void updateRestrictedLabelGeometry();
void updateScrollGeometry(QSize oldSize);
void updateFooterGeometry();
void handleScroll(); void handleScroll();
QImage grabForAnimation(); 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 } // 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) Reactions::Reactions(not_null<Session*> owner)
: _owner(owner) : _owner(owner)
, _repaintTimer([=] { repaintCollected(); }) { , _repaintTimer([=] { repaintCollected(); }) {

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once #pragma once
#include "base/timer.h" #include "base/timer.h"
#include "data/data_message_reaction_id.h"
namespace Lottie { namespace Lottie {
class Icon; class Icon;
@ -18,34 +19,6 @@ namespace Data {
class DocumentMedia; class DocumentMedia;
class Session; 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 { struct Reaction {
ReactionId id; ReactionId id;
QString title; QString title;

View file

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

View file

@ -21,6 +21,7 @@ class Session;
namespace Data { namespace Data {
struct Reaction; struct Reaction;
struct ReactionsFilter;
template <typename ChangeType, typename Error, typename Generator> template <typename ChangeType, typename Error, typename Generator>
inline auto FlagsValueWithMask( inline auto FlagsValueWithMask(
@ -133,10 +134,9 @@ inline auto PeerFullFlagValue(
int size, int size,
ImageRoundRadius radius); 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); not_null<PeerData*> peer);
[[nodiscard]] auto PeerAllowedReactionsValue(not_null<PeerData*> peer)
-> rpl::producer<std::optional<base::flat_set<QString>>>;
[[nodiscard]] rpl::producer<int> UniqueReactionsLimitValue( [[nodiscard]] rpl::producer<int> UniqueReactionsLimitValue(
not_null<Main::Session*> session); 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_context_menu.h"
#include "history/view/history_view_quick_action.h" #include "history/view/history_view_quick_action.h"
#include "history/view/history_view_react_button.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/view/history_view_emoji_interactions.h"
#include "history/history_item_components.h" #include "history/history_item_components.h"
#include "history/history_item_text.h" #include "history/history_item_text.h"
@ -345,6 +346,7 @@ HistoryInner::HistoryInner(
Data::UniqueReactionsLimitValue(&controller->session()), Data::UniqueReactionsLimitValue(&controller->session()),
[=](QRect updated) { update(updated); }, [=](QRect updated) { update(updated); },
controller->cachedReactionIconFactory().createMethod())) controller->cachedReactionIconFactory().createMethod()))
, _reactionsSelector(std::make_unique<HistoryView::Reactions::Selector>())
, _touchSelectTimer([=] { onTouchSelect(); }) , _touchSelectTimer([=] { onTouchSelect(); })
, _touchScrollTimer([=] { onTouchScrollTimer(); }) , _touchScrollTimer([=] { onTouchScrollTimer(); })
, _scrollDateCheck([this] { scrollDateCheck(); }) , _scrollDateCheck([this] { scrollDateCheck(); })
@ -393,28 +395,25 @@ HistoryInner::HistoryInner(
_controller->emojiInteractions().playStarted(_peer, std::move(emoji)); _controller->emojiInteractions().playStarted(_peer, std::move(emoji));
}, lifetime()); }, lifetime());
using ChosenReaction = HistoryView::Reactions::Manager::Chosen; rpl::merge(
_reactionsManager->chosen( _reactionsManager->chosen(),
_reactionsSelector->chosen()
) | rpl::start_with_next([=](ChosenReaction reaction) { ) | rpl::start_with_next([=](ChosenReaction reaction) {
const auto item = session().data().message(reaction.context); _reactionsManager->updateButton({});
if (!item reactionChosen(reaction);
|| Window::ShowReactPremiumError( }, lifetime());
_reactionsManager->setExternalSelectorShown(_reactionsSelector->shown());
_reactionsManager->expandSelectorRequests(
) | rpl::start_with_next([=](ReactionExpandRequest request) {
if (request.expanded) {
_reactionsSelector->show(
_controller, _controller,
item, this,
reaction.id)) { request.context,
return; request.button);
} } else {
item->toggleReaction(reaction.id); _reactionsSelector->hide();
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),
});
}
} }
}, lifetime()); }, lifetime());
@ -463,7 +462,7 @@ HistoryInner::HistoryInner(
HistoryView::Reactions::SetupManagerList( HistoryView::Reactions::SetupManagerList(
_reactionsManager.get(), _reactionsManager.get(),
&session(), &session(),
Data::PeerAllowedReactionsValue(_peer)); Data::PeerReactionsFilterValue(_peer));
controller->adaptive().chatWideValue( controller->adaptive().chatWideValue(
) | rpl::start_with_next([=](bool wide) { ) | rpl::start_with_next([=](bool wide) {
@ -478,6 +477,31 @@ HistoryInner::HistoryInner(
setupSharingDisallowed(); 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 { Main::Session &HistoryInner::session() const {
return _controller->session(); return _controller->session();
} }
@ -1925,10 +1949,11 @@ void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) {
void HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const { void HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const {
const auto favorite = session().data().reactions().favorite(); const auto favorite = session().data().reactions().favorite();
const auto allowed = _reactionsManager->allowedSublist(); const auto &filter = _reactionsManager->filter();
if (allowed if (favorite.emoji().isEmpty() && !filter.customAllowed) {
&& (favorite.emoji().isEmpty() return;
|| !allowed->contains(favorite.emoji()))) { } else if (filter.allowed
&& !filter.allowed->contains(favorite.emoji())) {
return; return;
} }
const auto item = view->data(); const auto item = view->data();

View file

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

View file

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

View file

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

View file

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

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::optional<base::flat_set<QString>>> override; -> rpl::producer<Data::ReactionsFilter> override;
void listShowPremiumToast(not_null<DocumentData*> document) override; void listShowPremiumToast(not_null<DocumentData*> document) override;
protected: protected:

View file

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

View file

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

View file

@ -143,7 +143,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::optional<base::flat_set<QString>>> override; ->rpl::producer<Data::ReactionsFilter> override;
void listShowPremiumToast(not_null<DocumentData*> document) override; void listShowPremiumToast(not_null<DocumentData*> document) override;
protected: protected:

View file

@ -1355,9 +1355,9 @@ CopyRestrictionType ScheduledWidget::listSelectRestrictionType() {
} }
auto ScheduledWidget::listAllowedReactionsValue() auto ScheduledWidget::listAllowedReactionsValue()
-> rpl::producer<std::optional<base::flat_set<QString>>> { -> rpl::producer<Data::ReactionsFilter> {
const auto empty = base::flat_set<QString>(); 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( void ScheduledWidget::listShowPremiumToast(

View file

@ -128,7 +128,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::optional<base::flat_set<QString>>> override; -> rpl::producer<Data::ReactionsFilter> override;
void listShowPremiumToast(not_null<DocumentData*> document) override; void listShowPremiumToast(not_null<DocumentData*> document) override;
protected: protected:

View file

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