Implement reactions selector above the menu.

This commit is contained in:
John Preston 2022-08-19 15:08:11 +03:00
parent f658cb7e99
commit bd42c23999
34 changed files with 3023 additions and 1074 deletions

View File

@ -671,6 +671,8 @@ PRIVATE
history/view/reactions/history_view_reactions_list.h
history/view/reactions/history_view_reactions_selector.cpp
history/view/reactions/history_view_reactions_selector.h
history/view/reactions/history_view_reactions_strip.cpp
history/view/reactions/history_view_reactions_strip.h
history/view/reactions/history_view_reactions_tabs.cpp
history/view/reactions/history_view_reactions_tabs.h
history/view/history_view_bottom_info.cpp

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

View File

@ -17,7 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_message.h"
#include "history/view/history_view_element.h"
#include "history/view/reactions/history_view_reactions_button.h"
#include "history/view/reactions/history_view_reactions_strip.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "boxes/premium_preview_box.h"

View File

@ -316,3 +316,8 @@ reactStripExtend: margins(21px, 49px, 39px, 0px);
reactStripHeight: 40px;
reactStripSize: 32px;
reactStripSkip: 7px;
reactStripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg },
{ "chat/reactions_bubble", windowBg },
};
reactStripBubbleRight: 20px;

View File

@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_peer_values.h"
#include "data/stickers/data_custom_emoji.h"
#include "lottie/lottie_icon.h"
#include "storage/localimageloader.h"
@ -43,6 +44,67 @@ constexpr auto kSizeForDownscale = 64;
} // namespace
PossibleItemReactions LookupPossibleReactions(not_null<HistoryItem*> item) {
if (!item->canReact()) {
return {};
}
auto result = PossibleItemReactions();
const auto peer = item->history()->peer;
const auto session = &peer->session();
const auto reactions = &session->data().reactions();
const auto &full = reactions->list(Reactions::Type::Active);
const auto &all = item->reactions();
const auto my = item->chosenReaction();
auto myIsUnique = false;
for (const auto &[id, count] : all) {
if (count == 1 && id == my) {
myIsUnique = true;
}
}
const auto notMineCount = int(all.size()) - (myIsUnique ? 1 : 0);
const auto limit = UniqueReactionsLimit(peer);
if (limit > 0 && notMineCount >= limit) {
result.recent.reserve(all.size());
for (const auto &reaction : full) {
const auto id = reaction.id;
if (all.contains(id)) {
result.recent.push_back(&reaction);
}
}
} else {
const auto filter = PeerReactionsFilter(peer);
result.recent.reserve(filter.allowed
? filter.allowed->size()
: full.size());
for (const auto &reaction : full) {
const auto id = reaction.id;
const auto emoji = filter.allowed ? id.emoji() : QString();
if (filter.allowed
&& (emoji.isEmpty() || !filter.allowed->contains(emoji))) {
continue;
} else if (reaction.premium
&& !session->premium()
&& !all.contains(id)) {
if (session->premiumPossible()) {
result.morePremiumAvailable = true;
}
continue;
} else {
result.recent.push_back(&reaction);
}
}
result.customAllowed = session->premium() && peer->isUser();
}
const auto i = ranges::find(
result.recent,
reactions->favorite(),
&Reaction::id);
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
}
return result;
}
Reactions::Reactions(not_null<Session*> owner)
: _owner(owner)
, _repaintTimer([=] { repaintCollected(); }) {

View File

@ -37,6 +37,15 @@ struct Reaction {
bool premium = false;
};
struct PossibleItemReactions {
std::vector<not_null<const Reaction*>> recent;
bool morePremiumAvailable = false;
bool customAllowed = false;
};
[[nodiscard]] PossibleItemReactions LookupPossibleReactions(
not_null<HistoryItem*> item);
class Reactions final {
public:
explicit Reactions(not_null<Session*> owner);
@ -115,7 +124,7 @@ private:
ReactionId _favorite;
base::flat_map<
not_null<DocumentData*>,
std::shared_ptr<Data::DocumentMedia>> _iconsCache;
std::shared_ptr<DocumentMedia>> _iconsCache;
rpl::event_stream<> _updated;
mtpRequestId _requestId = 0;

View File

@ -542,8 +542,8 @@ int UniqueReactionsLimit(not_null<PeerData*> peer) {
}
rpl::producer<int> UniqueReactionsLimitValue(
not_null<Main::Session*> session) {
const auto config = &session->account().appConfig();
not_null<PeerData*> peer) {
const auto config = &peer->session().account().appConfig();
return config->value(
) | rpl::map([=] {
return UniqueReactionsLimit(config);

View File

@ -140,6 +140,6 @@ inline auto PeerFullFlagValue(
[[nodiscard]] int UniqueReactionsLimit(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<int> UniqueReactionsLimitValue(
not_null<Main::Session*> session);
not_null<PeerData*> peer);
} // namespace Data

View File

@ -342,10 +342,8 @@ HistoryInner::HistoryInner(
, _reactionsManager(
std::make_unique<HistoryView::Reactions::Manager>(
this,
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(); })
@ -394,26 +392,16 @@ HistoryInner::HistoryInner(
_controller->emojiInteractions().playStarted(_peer, std::move(emoji));
}, lifetime());
rpl::merge(
_reactionsManager->chosen(),
_reactionsSelector->chosen()
_reactionsManager->chosen(
) | rpl::start_with_next([=](ChosenReaction reaction) {
_reactionsManager->updateButton({});
reactionChosen(reaction);
}, lifetime());
_reactionsManager->setExternalSelectorShown(_reactionsSelector->shown());
_reactionsManager->expandSelectorRequests(
) | rpl::start_with_next([=](ReactionExpandRequest request) {
if (request.expanded) {
_reactionsSelector->show(
_controller,
this,
request.context,
request.button);
} else {
_reactionsSelector->hide();
}
_reactionsManager->premiumPromoChosen(
) | rpl::start_with_next([=](FullMsgId context) {
_reactionsManager->updateButton({});
premiumPromoChosen(context);
}, lifetime());
session().data().itemRemoved(
@ -448,7 +436,6 @@ HistoryInner::HistoryInner(
return item->mainView() != nullptr;
}) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
item->mainView()->itemDataChanged();
_reactionsManager->updateUniqueLimit(item);
}, lifetime());
session().changes().historyUpdates(
@ -460,8 +447,7 @@ HistoryInner::HistoryInner(
HistoryView::Reactions::SetupManagerList(
_reactionsManager.get(),
&session(),
Data::PeerReactionsFilterValue(_peer));
_reactionsItem.value());
controller->adaptive().chatWideValue(
) | rpl::start_with_next([=](bool wide) {
@ -477,8 +463,6 @@ HistoryInner::HistoryInner(
}
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(
@ -501,6 +485,12 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) {
}
}
void HistoryInner::premiumPromoChosen(FullMsgId context) {
if (const auto item = session().data().message(context)) {
ShowPremiumPromoBox(_controller, item);
}
}
Main::Session &HistoryInner::session() const {
return _controller->session();
}
@ -1740,6 +1730,9 @@ void HistoryInner::itemRemoved(not_null<const HistoryItem*> item) {
return;
}
if (_reactionsItem.current() == item) {
_reactionsItem = nullptr;
}
_animatedStickersPlayed.remove(item);
_reactionsManager->remove(item->fullId());
@ -1947,16 +1940,13 @@ void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) {
}
void HistoryInner::toggleFavoriteReaction(not_null<Element*> view) const {
const auto favorite = session().data().reactions().favorite();
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();
if (Window::ShowReactPremiumError(_controller, item, favorite)) {
const auto favorite = session().data().reactions().favorite();
if (!ranges::contains(
Data::LookupPossibleReactions(item).recent,
favorite,
&Data::Reaction::id)
|| Window::ShowReactPremiumError(_controller, item, favorite)) {
return;
} else if (item->chosenReaction() != favorite) {
if (const auto top = itemTop(view); top >= 0) {
@ -2454,7 +2444,13 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
? Element::Hovered()->data().get()
: nullptr;
const auto attached = reactItem
? AttachSelectorToMenu(_menu.get(), desiredPosition, reactItem)
? AttachSelectorToMenu(
_menu.get(),
desiredPosition,
reactItem,
[=](ChosenReaction reaction) { reactionChosen(reaction); },
[=](FullMsgId context) { premiumPromoChosen(context); },
_controller->cachedReactionIconFactory().createMethod())
: AttachSelectorResult::Skipped;
if (attached == AttachSelectorResult::Failed) {
_menu = nullptr;
@ -3417,7 +3413,7 @@ void HistoryInner::mouseActionUpdate() {
m,
reactionState));
if (changed) {
_reactionsManager->updateUniqueLimit(item);
_reactionsItem = item;
}
if (view->pointState(m) != PointState::Outside) {
if (Element::Hovered() != view) {

View File

@ -36,9 +36,7 @@ class Element;
namespace HistoryView::Reactions {
class Manager;
class Selector;
struct ChosenReaction;
struct ExpandRequest;
struct ButtonParameters;
} // namespace HistoryView::Reactions
@ -229,7 +227,6 @@ private:
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 {
@ -402,6 +399,7 @@ private:
-> HistoryView::Reactions::ButtonParameters;
void toggleFavoriteReaction(not_null<Element*> view) const;
void reactionChosen(const ChosenReaction &reaction);
void premiumPromoChosen(FullMsgId context);
void setupSharingDisallowed();
[[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const;
@ -464,7 +462,7 @@ private:
std::unique_ptr<VideoUserpic>> _videoUserpics;
std::unique_ptr<HistoryView::Reactions::Manager> _reactionsManager;
std::unique_ptr<HistoryView::Reactions::Selector> _reactionsSelector;
rpl::variable<HistoryItem*> _reactionsItem;
MouseAction _mouseAction = MouseAction::None;
TextSelectType _mouseSelectType = TextSelectType::Letters;

View File

@ -277,7 +277,6 @@ ListWidget::ListWidget(
, _reactionsManager(
std::make_unique<Reactions::Manager>(
this,
Data::UniqueReactionsLimitValue(&controller->session()),
[=](QRect updated) { update(updated); },
controller->cachedReactionIconFactory().createMethod()))
, _scrollDateCheck([this] { scrollDateCheck(); })
@ -379,10 +378,17 @@ ListWidget::ListWidget(
}
}, lifetime());
_reactionsManager->premiumPromoChosen(
) | rpl::start_with_next([=] {
_reactionsManager->updateButton({});
if (const auto item = _reactionsItem.current()) {
ShowPremiumPromoBox(_controller, item);
}
}, lifetime());
Reactions::SetupManagerList(
_reactionsManager.get(),
&session(),
_delegate->listAllowedReactionsValue());
_reactionsItem.value());
controller->adaptive().chatWideValue(
) | rpl::start_with_next([=](bool wide) {
@ -2115,16 +2121,13 @@ void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {
}
void ListWidget::toggleFavoriteReaction(not_null<Element*> view) const {
const auto favorite = session().data().reactions().favorite();
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();
if (Window::ShowReactPremiumError(_controller, item, favorite)) {
const auto favorite = session().data().reactions().favorite();
if (!ranges::contains(
Data::LookupPossibleReactions(item).recent,
favorite,
&Data::Reaction::id)
|| Window::ShowReactPremiumError(_controller, item, favorite)) {
return;
} else if (item->chosenReaction() != favorite) {
if (const auto top = itemTop(view); top >= 0) {
@ -2727,7 +2730,7 @@ void ListWidget::mouseActionUpdate() {
reactionState)
: Reactions::ButtonParameters());
if (viewChanged && view) {
_reactionsManager->updateUniqueLimit(item);
_reactionsItem = item;
}
TextState dragState;
@ -3161,6 +3164,9 @@ void ListWidget::viewReplaced(not_null<const Element*> was, Element *now) {
}
void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
if (_reactionsItem.current() == item) {
_reactionsItem = nullptr;
}
if (_selectedTextItem == item) {
clearTextSelection();
}

View File

@ -586,6 +586,7 @@ private:
base::unique_qptr<Ui::RpWidget> _emptyInfo = nullptr;
std::unique_ptr<HistoryView::Reactions::Manager> _reactionsManager;
rpl::variable<HistoryItem*> _reactionsItem;
int _minHeight = 0;
int _visibleTop = 0;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,373 @@
/*
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 "ui/effects/animations.h"
#include "ui/effects/round_area_with_shadow.h"
#include "ui/widgets/scroll_area.h"
#include "data/data_message_reaction_id.h"
#include "ui/chat/chat_style.h"
namespace Ui {
struct ChatPaintContext;
class PopupMenu;
} // namespace Ui
namespace Data {
struct Reaction;
class DocumentMedia;
} // namespace Data
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
struct TextState;
} // namespace HistoryView
namespace Main {
class Session;
} // namespace Main
namespace Lottie {
class Icon;
} // namespace Lottie
namespace HistoryView::Reactions {
enum class ExpandDirection {
Up,
Down,
};
struct ButtonParameters {
[[nodiscard]] ButtonParameters translated(QPoint delta) const {
auto result = *this;
result.center += delta;
result.pointer += delta;
return result;
}
FullMsgId context;
QPoint center;
QPoint pointer;
QPoint globalPointer;
int reactionsCount = 1;
int visibleTop = 0;
int visibleBottom = 0;
bool outside = false;
bool cursorLeft = false;
};
enum class ButtonState {
Hidden,
Shown,
Active,
Inside,
};
class Button final {
public:
Button(
Fn<void(QRect)> update,
ButtonParameters parameters,
Fn<void(bool)> toggleExpanded,
Fn<void()> hide);
~Button();
void applyParameters(ButtonParameters parameters);
void expandWithoutCustom();
using State = ButtonState;
void applyState(State state);
[[nodiscard]] bool expandUp() const;
[[nodiscard]] bool isHidden() const;
[[nodiscard]] QRect geometry() const;
[[nodiscard]] int expandedHeight() const;
[[nodiscard]] int scroll() const;
[[nodiscard]] int scrollMax() const;
[[nodiscard]] float64 currentScale() const;
[[nodiscard]] float64 currentOpacity() const;
[[nodiscard]] float64 expandAnimationOpacity(float64 expandRatio) const;
[[nodiscard]] int expandAnimationScroll(float64 expandRatio) const;
[[nodiscard]] bool consumeWheelEvent(not_null<QWheelEvent*> e);
[[nodiscard]] static float64 ScaleForState(State state);
[[nodiscard]] static float64 OpacityForScale(float64 scale);
private:
enum class CollapseType {
Scroll,
Fade,
};
void updateGeometry(Fn<void(QRect)> update);
void applyState(State satte, Fn<void(QRect)> update);
void applyParameters(
ButtonParameters parameters,
Fn<void(QRect)> update);
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;
Ui::Animations::Simple _opacityAnimation;
Ui::Animations::Simple _heightAnimation;
QRect _collapsed;
QRect _geometry;
int _expandedInnerHeight = 0;
int _expandedHeight = 0;
int _finalHeight = 0;
int _scroll = 0;
ExpandDirection _expandDirection = ExpandDirection::Up;
CollapseType _collapseType = CollapseType::Scroll;
base::Timer _expandTimer;
base::Timer _hideTimer;
std::optional<QPoint> _lastGlobalPosition;
};
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)>;
class Manager final : public base::has_weak_ptr {
public:
Manager(
QWidget *wheelEventsTarget,
rpl::producer<int> uniqueLimitValue,
Fn<void(QRect)> buttonUpdate,
IconFactory iconFactory);
~Manager();
using ReactionId = ::Data::ReactionId;
void applyList(
const std::vector<Data::Reaction> &list,
const ReactionId &favorite,
bool premiumPossible);
void updateFilter(Data::ReactionsFilter filter);
void updateAllowSendingPremium(bool allow);
[[nodiscard]] const Data::ReactionsFilter &filter() const;
void updateUniqueLimit(not_null<HistoryItem*> item);
void updateButton(ButtonParameters parameters);
void paint(Painter &p, const PaintContext &context);
[[nodiscard]] TextState buttonTextState(QPoint position) const;
void remove(FullMsgId context);
[[nodiscard]] bool consumeWheelEvent(not_null<QWheelEvent*> e);
[[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;
void startEffectsCollection();
[[nodiscard]] auto currentReactionPaintInfo()
-> not_null<Ui::ReactionPaintInfo*>;
void recordCurrentReactionEffect(FullMsgId itemId, QPoint origin);
bool showContextMenu(
QWidget *parent,
QContextMenuEvent *e,
const ReactionId &favorite);
[[nodiscard]] rpl::producer<ReactionId> faveRequests() const;
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private:
struct ReactionDocument {
std::shared_ptr<Data::DocumentMedia> media;
std::shared_ptr<Lottie::Icon> icon;
};
struct ReactionIcons {
ReactionId id;
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;
bool premium = false;
bool premiumLock = false;
mutable bool selected = false;
mutable bool selectAnimated = false;
};
static constexpr auto kFramesCount = Ui::RoundAreaWithShadow::kFramesCount;
void applyListFilters();
void showButtonDelayed();
void stealWheelEvents(not_null<QWidget*> target);
[[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(
Painter &p,
const PaintContext &context,
not_null<Button*> button);
void paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button,
int frame,
float64 scale);
void paintAllEmoji(
Painter &p,
not_null<Button*> button,
int scroll,
float64 scale,
QPoint position,
QPoint mainEmojiPosition);
void paintPremiumIcon(QPainter &p, QPoint position, QRectF target) const;
void paintInnerGradients(
Painter &p,
const QColor &background,
not_null<Button*> button,
int scroll,
float64 expandRatio);
void resolveMainReactionIcon();
void setMainReactionIcon();
void clearAppearAnimations();
[[nodiscard]] QRect cacheRect(int frameIndex, int columnIndex) const;
QRect validateEmoji(int frameIndex, float64 scale);
void setSelectedIcon(int index) const;
void clearStateForHidden(ReactionIcons &icon);
void clearStateForSelectFinished(ReactionIcons &icon);
[[nodiscard]] QMargins innerMargins() const;
[[nodiscard]] QRect buttonInner() const;
[[nodiscard]] QRect buttonInner(not_null<Button*> button) const;
[[nodiscard]] ClickHandlerPtr computeButtonLink(QPoint position) const;
[[nodiscard]] ClickHandlerPtr resolveButtonLink(
const ReactionIcons &reaction) const;
void updateCurrentButton() const;
[[nodiscard]] bool onlyMainEmojiVisible() const;
[[nodiscard]] bool checkIconLoaded(ReactionDocument &entry) const;
void loadIcons();
void checkIcons();
const IconFactory _iconFactory;
rpl::event_stream<ChosenReaction> _chosen;
rpl::event_stream<ExpandRequest> _expandSelectorRequests;
std::vector<ReactionIcons> _list;
ReactionId _favorite;
Data::ReactionsFilter _filter;
QSize _outer;
QRect _inner;
Ui::RoundAreaWithShadow _cachedRound;
QImage _emojiParts;
QImage _expandedBuffer;
QColor _gradientBackground;
QImage _topGradient;
QImage _bottomGradient;
std::array<bool, kFramesCount> _validEmoji = { { false } };
QColor _gradient;
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<not_null<ReactionIcons*>> _icons;
std::optional<ReactionIcons> _premiumIcon;
rpl::lifetime _loadCacheLifetime;
bool _showingAll = false;
bool _allowSendingPremium = false;
bool _premiumPossible = false;
mutable int _selectedIcon = -1;
std::optional<ButtonParameters> _scheduledParameters;
base::Timer _buttonShowTimer;
const Fn<void(QRect)> _buttonUpdate;
std::unique_ptr<Button> _button;
std::vector<std::unique_ptr<Button>> _buttonHiding;
FullMsgId _buttonContext;
base::flat_set<ReactionId> _buttonAlreadyList;
int _buttonAlreadyNotMineCount = 0;
mutable base::flat_map<ReactionId, ClickHandlerPtr> _reactionsLinks;
Fn<Fn<void()>(ReactionId)> _createChooseCallback;
base::flat_map<FullMsgId, QRect> _activeEffectAreas;
Ui::ReactionPaintInfo _currentReactionInfo;
base::flat_map<FullMsgId, Ui::ReactionPaintInfo> _collectedEffects;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<ReactionId> _faveRequests;
bool _externalSelectorShown = false;
rpl::lifetime _lifetime;
};
class CachedIconFactory final {
public:
CachedIconFactory() = default;
CachedIconFactory(const CachedIconFactory &other) = delete;
CachedIconFactory &operator=(const CachedIconFactory &other) = delete;
[[nodiscard]] IconFactory createMethod();
private:
base::flat_map<
std::shared_ptr<Data::DocumentMedia>,
std::shared_ptr<Lottie::Icon>> _cache;
};
void SetupManagerList(
not_null<Manager*> manager,
not_null<Main::Session*> session,
rpl::producer<Data::ReactionsFilter> filter);
[[nodiscard]] std::shared_ptr<Lottie::Icon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,
int size);
} // namespace HistoryView

View File

@ -7,19 +7,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "base/unique_qptr.h"
#include "ui/effects/animations.h"
#include "ui/effects/round_area_with_shadow.h"
#include "ui/widgets/scroll_area.h"
#include "data/data_message_reaction_id.h"
#include "ui/chat/chat_style.h"
#include "history/view/reactions/history_view_reactions_strip.h"
#include "ui/chat/chat_style.h" // Ui::ReactionPaintInfo
namespace Ui {
struct ChatPaintContext;
struct ReactionPaintInfo;
class PopupMenu;
} // namespace Ui
namespace Data {
struct ReactionId;
struct Reaction;
struct PossibleItemReactions;
class DocumentMedia;
} // namespace Data
@ -32,10 +36,6 @@ namespace Main {
class Session;
} // namespace Main
namespace Lottie {
class Icon;
} // namespace Lottie
namespace HistoryView::Reactions {
enum class ExpandDirection {
@ -74,12 +74,10 @@ public:
Button(
Fn<void(QRect)> update,
ButtonParameters parameters,
Fn<void(bool)> toggleExpanded,
Fn<void()> hide);
~Button();
void applyParameters(ButtonParameters parameters);
void expandWithoutCustom();
using State = ButtonState;
void applyState(State state);
@ -113,7 +111,6 @@ private:
void updateExpandDirection(const ButtonParameters &parameters);
const Fn<void(QRect)> _update;
const Fn<void(bool)> _toggleExpanded;
State _state = State::Hidden;
float64 _finalScale = 0.;
@ -136,46 +133,17 @@ 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)>;
class Manager final : public base::has_weak_ptr {
public:
Manager(
QWidget *wheelEventsTarget,
rpl::producer<int> uniqueLimitValue,
Fn<void(QRect)> buttonUpdate,
IconFactory iconFactory);
~Manager();
using ReactionId = ::Data::ReactionId;
void applyList(
const std::vector<Data::Reaction> &list,
const ReactionId &favorite,
bool premiumPossible);
void updateFilter(Data::ReactionsFilter filter);
void updateAllowSendingPremium(bool allow);
[[nodiscard]] const Data::ReactionsFilter &filter() const;
void updateUniqueLimit(not_null<HistoryItem*> item);
void applyList(Data::PossibleItemReactions &&reactions);
void updateButton(ButtonParameters parameters);
void paint(Painter &p, const PaintContext &context);
@ -187,11 +155,12 @@ public:
[[nodiscard]] rpl::producer<ChosenReaction> chosen() const {
return _chosen.events();
}
[[nodiscard]] auto expandSelectorRequests() const
-> rpl::producer<ExpandRequest> {
return _expandSelectorRequests.events();
[[nodiscard]] rpl::producer<FullMsgId> premiumPromoChosen() const {
return _premiumPromoChosen.events();
}
[[nodiscard]] rpl::producer<FullMsgId> expandChosen() const {
return _expandChosen.events();
}
void setExternalSelectorShown(rpl::producer<bool> shown);
[[nodiscard]] std::optional<QRect> lookupEffectArea(
FullMsgId itemId) const;
@ -211,34 +180,11 @@ public:
}
private:
struct ReactionDocument {
std::shared_ptr<Data::DocumentMedia> media;
std::shared_ptr<Lottie::Icon> icon;
};
struct ReactionIcons {
ReactionId id;
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;
bool premium = false;
bool premiumLock = false;
mutable bool selected = false;
mutable bool selectAnimated = false;
};
static constexpr auto kFramesCount = Ui::RoundAreaWithShadow::kFramesCount;
void applyListFilters();
void showButtonDelayed();
void stealWheelEvents(not_null<QWidget*> target);
[[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(
@ -251,14 +197,6 @@ private:
not_null<Button*> button,
int frame,
float64 scale);
void paintAllEmoji(
Painter &p,
not_null<Button*> button,
int scroll,
float64 scale,
QPoint position,
QPoint mainEmojiPosition);
void paintPremiumIcon(QPainter &p, QPoint position, QRectF target) const;
void paintInnerGradients(
Painter &p,
const QColor &background,
@ -266,15 +204,8 @@ private:
int scroll,
float64 expandRatio);
void resolveMainReactionIcon();
void setMainReactionIcon();
void clearAppearAnimations();
[[nodiscard]] QRect cacheRect(int frameIndex, int columnIndex) const;
QRect validateEmoji(int frameIndex, float64 scale);
void setSelectedIcon(int index) const;
void clearStateForHidden(ReactionIcons &icon);
void clearStateForSelectFinished(ReactionIcons &icon);
[[nodiscard]] QMargins innerMargins() const;
[[nodiscard]] QRect buttonInner() const;
@ -282,45 +213,29 @@ private:
[[nodiscard]] ClickHandlerPtr computeButtonLink(QPoint position) const;
[[nodiscard]] ClickHandlerPtr resolveButtonLink(
const ReactionIcons &reaction) const;
const ReactionId &id) const;
void updateCurrentButton() const;
[[nodiscard]] bool onlyMainEmojiVisible() const;
[[nodiscard]] bool checkIconLoaded(ReactionDocument &entry) const;
void loadIcons();
void checkIcons();
const IconFactory _iconFactory;
rpl::event_stream<ChosenReaction> _chosen;
rpl::event_stream<ExpandRequest> _expandSelectorRequests;
std::vector<ReactionIcons> _list;
ReactionId _favorite;
Data::ReactionsFilter _filter;
QSize _outer;
QRect _inner;
Strip _strip;
Ui::RoundAreaWithShadow _cachedRound;
QImage _emojiParts;
QImage _expandedBuffer;
QColor _gradientBackground;
QImage _topGradient;
QImage _bottomGradient;
std::array<bool, kFramesCount> _validEmoji = { { false } };
QColor _gradient;
std::shared_ptr<Data::DocumentMedia> _mainReactionMedia;
std::shared_ptr<Lottie::Icon> _mainReactionIcon;
QImage _mainReactionImage;
rpl::lifetime _mainReactionLifetime;
rpl::event_stream<ChosenReaction> _chosen;
rpl::event_stream<FullMsgId> _premiumPromoChosen;
rpl::event_stream<FullMsgId> _expandChosen;
mutable base::flat_map<ReactionId, ClickHandlerPtr> _links;
mutable ClickHandlerPtr _premiumPromoLink;
mutable ClickHandlerPtr _expandLink;
rpl::variable<int> _uniqueLimit = 0;
base::flat_map<not_null<DocumentData*>, ReactionDocument> _loadCache;
std::vector<not_null<ReactionIcons*>> _icons;
std::optional<ReactionIcons> _premiumIcon;
rpl::lifetime _loadCacheLifetime;
bool _showingAll = false;
bool _allowSendingPremium = false;
bool _premiumPossible = false;
mutable int _selectedIcon = -1;
std::optional<ButtonParameters> _scheduledParameters;
base::Timer _buttonShowTimer;
@ -328,8 +243,6 @@ private:
std::unique_ptr<Button> _button;
std::vector<std::unique_ptr<Button>> _buttonHiding;
FullMsgId _buttonContext;
base::flat_set<ReactionId> _buttonAlreadyList;
int _buttonAlreadyNotMineCount = 0;
mutable base::flat_map<ReactionId, ClickHandlerPtr> _reactionsLinks;
Fn<Fn<void()>(ReactionId)> _createChooseCallback;
@ -340,31 +253,14 @@ private:
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<ReactionId> _faveRequests;
bool _externalSelectorShown = false;
rpl::lifetime _lifetime;
};
class CachedIconFactory final {
public:
CachedIconFactory() = default;
CachedIconFactory(const CachedIconFactory &other) = delete;
CachedIconFactory &operator=(const CachedIconFactory &other) = delete;
[[nodiscard]] IconFactory createMethod();
private:
base::flat_map<
std::shared_ptr<Data::DocumentMedia>,
std::shared_ptr<Lottie::Icon>> _cache;
};
void SetupManagerList(
not_null<Manager*> manager,
not_null<Main::Session*> session,
rpl::producer<Data::ReactionsFilter> filter);
rpl::producer<HistoryItem*> items);
[[nodiscard]] std::shared_ptr<Lottie::Icon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,

View File

@ -7,155 +7,76 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/reactions/history_view_reactions_selector.h"
#include "history/view/reactions/history_view_reactions_button.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_message_reactions.h"
#include "data/data_peer_values.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "ui/widgets/popup_menu.h"
#include "history/history.h"
#include "history/history_item.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_chat.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();
}
}
PopupSelector::PopupSelector(
Selector::Selector(
not_null<QWidget*> parent,
PossibleReactions reactions)
Data::PossibleItemReactions &&reactions,
IconFactory iconFactory)
: RpWidget(parent)
, _reactions(std::move(reactions))
, _cachedRound(
QSize(st::reactStripSkip * 2 + st::reactStripSize, st::reactStripHeight),
st::reactionCornerShadow,
st::reactStripHeight)
, _strip(
QRect(0, 0, st::reactStripSize, st::reactStripSize),
crl::guard(this, [=] { update(_inner); }),
std::move(iconFactory))
, _size(st::reactStripSize)
, _skipx(st::reactStripSkip)
, _skipy((st::reactStripHeight - st::reactStripSize) / 2)
, _skipBottom(st::reactStripHeight - st::reactStripSize - _skipy) {
setMouseTracking(true);
}
int PopupSelector::countWidth(int desiredWidth, int maxWidth) {
const auto added = _reactions.customAllowed
int Selector::countWidth(int desiredWidth, int maxWidth) {
const auto addedToMax = _reactions.customAllowed
|| _reactions.morePremiumAvailable;
const auto max = int(_reactions.recent.size()) + (addedToMax ? 1 : 0);
const auto possibleColumns = std::min(
(desiredWidth - 2 * _skipx + _size - 1) / _size,
(maxWidth - 2 * _skipx) / _size);
_columns = std::min(
possibleColumns,
int(_reactions.recent.size()) + (added ? 1 : 0));
_columns = std::min(possibleColumns, max);
_small = (possibleColumns - _columns > 1);
_recentRows = (_reactions.recent.size() + _columns - 1) / _columns;
_recentRows = (_strip.count() + _columns - 1) / _columns;
const auto added = (_columns < max || _reactions.customAllowed)
? Strip::AddedButton::Expand
: _reactions.morePremiumAvailable
? Strip::AddedButton::Premium
: Strip::AddedButton::None;
if (const auto cut = max - _columns) {
_strip.applyList(ranges::make_subrange(
begin(_reactions.recent),
end(_reactions.recent) - (cut + (addedToMax ? 0 : 1))
) | ranges::to_vector, added);
} else {
_strip.applyList(_reactions.recent, added);
}
_strip.clearAppearAnimations(false);
return std::max(2 * _skipx + _columns * _size, desiredWidth);
}
QMargins PopupSelector::extentsForShadow() const {
QMargins Selector::extentsForShadow() const {
return st::reactionCornerShadow;
}
int PopupSelector::extendTopForCategories() const {
int Selector::extendTopForCategories() const {
return st::emojiFooterHeight;
}
int PopupSelector::desiredHeight() const {
int Selector::desiredHeight() const {
return _reactions.customAllowed
? st::emojiPanMaxHeight
: (_skipy + _recentRows * _size + _skipBottom);
}
void PopupSelector::initGeometry(int innerTop) {
void Selector::initGeometry(int innerTop) {
const auto extents = extentsForShadow();
const auto parent = parentWidget()->rect();
const auto innerWidth = 2 * _skipx + _columns * _size;
@ -164,131 +85,185 @@ void PopupSelector::initGeometry(int innerTop) {
const auto height = innerHeight + extents.top() + extents.bottom();
const auto left = style::RightToLeft() ? 0 : (parent.width() - width);
const auto top = innerTop - extents.top();
setGeometry(left, top, width, height);
_inner = rect().marginsRemoved(extents);
const auto add = st::reactStripBubble.height() - extents.bottom();
_outer = QRect(0, 0, width, height);
setGeometry(_outer.marginsAdded({ 0, 0, 0, add }).translated(left, top));
_inner = _outer.marginsRemoved(extents);
}
void PopupSelector::updateShowState(
void Selector::updateShowState(
float64 progress,
float64 opacity,
bool appearing,
bool toggling) {
if (_appearing && !appearing && !_paintBuffer.isNull()) {
paintBackgroundToBuffer();
}
_appearing = appearing;
_toggling = toggling;
_appearProgress = progress;
_appearOpacity = opacity;
if (_appearing && isHidden()) {
show();
raise();
} else if (_toggling && !isHidden()) {
hide();
}
if (!_appearing && !_low) {
_low = true;
lower();
}
update();
}
void PopupSelector::paintAppearing(QPainter &p) {
void Selector::paintAppearing(QPainter &p) {
p.setOpacity(_appearOpacity);
const auto factor = style::DevicePixelRatio();
if (_appearBuffer.size() != size() * factor) {
_appearBuffer = _cachedRound.PrepareImage(size());
if (_paintBuffer.size() != size() * factor) {
_paintBuffer = _cachedRound.PrepareImage(size());
}
_appearBuffer.fill(st::defaultPopupMenu.menu.itemBg->c);
auto q = QPainter(&_appearBuffer);
_paintBuffer.fill(st::defaultPopupMenu.menu.itemBg->c);
auto q = QPainter(&_paintBuffer);
const auto extents = extentsForShadow();
const auto appearedWidth = anim::interpolate(
_skipx * 2 + _size,
_inner.width(),
_appearProgress);
const auto fullWidth = appearedWidth + extents.left() + extents.right();
const auto size = QSize(fullWidth, height());
const auto fullWidth = _inner.x() + appearedWidth + extents.right();
const auto size = QSize(fullWidth, _outer.height());
_strip.paint(
q,
{ _inner.x() + _skipx, _inner.y() + _skipy },
{ _size, 0 },
{ _inner.x(), _inner.y(), appearedWidth, _inner.height() },
1.,
false);
_cachedRound.setBackgroundColor(st::defaultPopupMenu.menu.itemBg->c);
_cachedRound.setShadowColor(st::shadowFg->c);
const auto radius = st::reactStripHeight / 2;
_cachedRound.overlayExpandedBorder(q, size, _appearProgress, radius, 1.);
q.setCompositionMode(QPainter::CompositionMode_Source);
q.fillRect(
QRect{ 0, size.height(), width(), height() - size.height() },
Qt::transparent);
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
paintBubble(q, appearedWidth);
q.end();
p.drawImage(
QPoint(),
_appearBuffer,
QRect(QPoint(), size * style::DevicePixelRatio()));
_paintBuffer,
QRect(QPoint(), QSize(fullWidth, height()) * factor));
}
void PopupSelector::paintBg(QPainter &p) {
_cachedRound.FillWithImage(p, rect(), _cachedRound.validateFrame(0, 1.));
void Selector::paintBackgroundToBuffer() {
if (_paintBuffer.size() != size() * style::DevicePixelRatio()) {
_paintBuffer = _cachedRound.PrepareImage(size());
}
_paintBuffer.fill(Qt::transparent);
auto p = QPainter(&_paintBuffer);
_cachedRound.FillWithImage(p, _outer, _cachedRound.validateFrame(0, 1.));
paintBubble(p, _inner.width());
}
void PopupSelector::paintEvent(QPaintEvent *e) {
void Selector::paintHorizontal(QPainter &p) {
if (_paintBuffer.isNull()) {
paintBackgroundToBuffer();
}
p.drawImage(0, 0, _paintBuffer);
const auto extents = extentsForShadow();
_strip.paint(
p,
{ _inner.x() + _skipx, _inner.y() + _skipy },
{ _size, 0 },
_inner,
1.,
false);
}
void Selector::paintBubble(QPainter &p, int innerWidth) {
const auto &bubble = st::reactStripBubble;
const auto bubbleRight = std::min(
st::reactStripBubbleRight,
(innerWidth - bubble.width()) / 2);
bubble.paint(
p,
_inner.x() + innerWidth - bubbleRight - bubble.width(),
_inner.y() + _inner.height(),
width());
}
void Selector::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
if (_appearing) {
paintAppearing(p);
} else {
paintBg(p);
paintHorizontal(p);
}
}
[[nodiscard]] PossibleReactions LookupPossibleReactions(
not_null<HistoryItem*> item) {
if (!item->canReact()) {
return {};
void Selector::mouseMoveEvent(QMouseEvent *e) {
setSelected(lookupSelectedIndex(e->pos()));
}
int Selector::lookupSelectedIndex(QPoint position) const {
const auto p = position - _inner.topLeft();
const auto max = _strip.count();
const auto index = p.x() / _size;
if (p.x() >= 0 && p.y() >= 0 && p.y() < _inner.height() && index < max) {
return index;
}
auto result = PossibleReactions();
const auto peer = item->history()->peer;
const auto session = &peer->session();
const auto reactions = &session->data().reactions();
const auto &full = reactions->list(Data::Reactions::Type::Active);
const auto &all = item->reactions();
const auto my = item->chosenReaction();
auto myIsUnique = false;
for (const auto &[id, count] : all) {
if (count == 1 && id == my) {
myIsUnique = true;
return -1;
}
void Selector::setSelected(int index) {
_strip.setSelected(index);
const auto over = (index >= 0);
if (_over != over) {
_over = over;
setCursor(over ? style::cur_pointer : style::cur_default);
if (over) {
Ui::Integration::Instance().registerLeaveSubscription(this);
} else {
Ui::Integration::Instance().unregisterLeaveSubscription(this);
}
}
const auto notMineCount = int(all.size()) - (myIsUnique ? 1 : 0);
const auto limit = Data::UniqueReactionsLimit(peer);
if (limit > 0 && notMineCount >= limit) {
result.recent.reserve(all.size());
for (const auto &reaction : full) {
const auto id = reaction.id;
if (all.contains(id)) {
result.recent.push_back(id);
}
}
}
void Selector::leaveEventHook(QEvent *e) {
setSelected(-1);
}
void Selector::mousePressEvent(QMouseEvent *e) {
_pressed = lookupSelectedIndex(e->pos());
}
void Selector::mouseReleaseEvent(QMouseEvent *e) {
if (_pressed != lookupSelectedIndex(e->pos())) {
return;
}
_pressed = -1;
const auto selected = _strip.selected();
if (selected == Strip::AddedButton::Premium) {
_premiumPromoChosen.fire({});
} else if (selected == Strip::AddedButton::Expand) {
} else {
const auto filter = Data::PeerReactionsFilter(peer);
result.recent.reserve(filter.allowed
? filter.allowed->size()
: full.size());
for (const auto &reaction : full) {
const auto id = reaction.id;
const auto emoji = filter.allowed ? id.emoji() : QString();
if (filter.allowed
&& (emoji.isEmpty() || !filter.allowed->contains(emoji))) {
continue;
} else if (reaction.premium
&& !session->premium()
&& !all.contains(id)) {
if (session->premiumPossible()) {
result.morePremiumAvailable = true;
}
continue;
} else {
result.recent.push_back(id);
}
const auto id = std::get_if<Data::ReactionId>(&selected);
if (id && !id->empty()) {
_chosen.fire({ .id = *id });
}
result.customAllowed = peer->isUser();
}
const auto i = ranges::find(result.recent, reactions->favorite());
if (i != end(result.recent) && i != begin(result.recent)) {
std::rotate(begin(result.recent), i, i + 1);
}
return result;
}
bool AdjustMenuGeometryForSelector(
not_null<Ui::PopupMenu*> menu,
QPoint desiredPosition,
not_null<PopupSelector*> selector) {
not_null<Selector*> selector) {
const auto extend = st::reactStripExtend;
const auto added = extend.left() + extend.right();
const auto desiredWidth = menu->menu()->width() + added;
@ -348,14 +323,18 @@ bool AdjustMenuGeometryForSelector(
AttachSelectorResult AttachSelectorToMenu(
not_null<Ui::PopupMenu*> menu,
QPoint desiredPosition,
not_null<HistoryItem*> item) {
auto reactions = LookupPossibleReactions(item);
not_null<HistoryItem*> item,
Fn<void(ChosenReaction)> chosen,
Fn<void(FullMsgId)> showPremiumPromo,
IconFactory iconFactory) {
auto reactions = Data::LookupPossibleReactions(item);
if (reactions.recent.empty() && !reactions.morePremiumAvailable) {
return AttachSelectorResult::Skipped;
}
const auto selector = Ui::CreateChild<PopupSelector>(
const auto selector = Ui::CreateChild<Selector>(
menu.get(),
std::move(reactions));
std::move(reactions),
std::move(iconFactory));
if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) {
return AttachSelectorResult::Failed;
}
@ -364,6 +343,19 @@ AttachSelectorResult AttachSelectorToMenu(
selector->initGeometry(selectorInnerTop);
selector->show();
const auto itemId = item->fullId();
selector->chosen() | rpl::start_with_next([=](ChosenReaction reaction) {
menu->hideMenu();
reaction.context = itemId;
chosen(std::move(reaction));
}, selector->lifetime());
selector->premiumPromoChosen() | rpl::start_with_next([=] {
menu->hideMenu();
showPremiumPromo(itemId);
}, selector->lifetime());
const auto correctTop = selector->y();
menu->showStateValue(
) | rpl::start_with_next([=](Ui::PopupMenu::ShowState state) {
@ -378,7 +370,7 @@ AttachSelectorResult AttachSelectorToMenu(
selector->move(selector->x(), correctTop + add);
}
selector->updateShowState(
std::min(state.widthProgress, state.heightProgress),
state.widthProgress * state.heightProgress,
state.opacity,
state.appearing,
state.toggling);

View File

@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "history/view/reactions/history_view_reactions_strip.h"
#include "data/data_message_reactions.h"
#include "base/unique_qptr.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/round_area_with_shadow.h"
@ -30,41 +32,12 @@ class PopupMenu;
namespace HistoryView::Reactions {
struct ChosenReaction;
class Selector final {
class Selector final : public Ui::RpWidget {
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;
};
struct PossibleReactions {
std::vector<Data::ReactionId> recent;
bool morePremiumAvailable = false;
bool customAllowed = false;
};
class PopupSelector final : public Ui::RpWidget {
public:
PopupSelector(
Selector(
not_null<QWidget*> parent,
PossibleReactions reactions);
Data::PossibleItemReactions &&reactions,
IconFactory iconFactory);
int countWidth(int desiredWidth, int maxWidth);
[[nodiscard]] QMargins extentsForShadow() const;
@ -72,6 +45,13 @@ public:
[[nodiscard]] int desiredHeight() const;
void initGeometry(int innerTop);
[[nodiscard]] rpl::producer<ChosenReaction> chosen() const {
return _chosen.events();
}
[[nodiscard]] rpl::producer<> premiumPromoChosen() const {
return _premiumPromoChosen.events();
}
void updateShowState(
float64 progress,
float64 opacity,
@ -81,17 +61,32 @@ public:
private:
static constexpr int kFramesCount = 32;
void paintEvent(QPaintEvent *e);
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void paintAppearing(QPainter &p);
void paintBg(QPainter &p);
void paintHorizontal(QPainter &p);
void paintBubble(QPainter &p, int innerWidth);
void paintBackgroundToBuffer();
PossibleReactions _reactions;
QImage _appearBuffer;
[[nodiscard]] int lookupSelectedIndex(QPoint position) const;
void setSelected(int index);
const Data::PossibleItemReactions _reactions;
Ui::RoundAreaWithShadow _cachedRound;
Strip _strip;
rpl::event_stream<ChosenReaction> _chosen;
rpl::event_stream<> _premiumPromoChosen;
QImage _paintBuffer;
float64 _appearProgress = 0.;
float64 _appearOpacity = 0.;
QRect _inner;
QRect _outer;
QMargins _padding;
int _size = 0;
int _recentRows = 0;
@ -99,9 +94,12 @@ private:
int _skipx = 0;
int _skipy = 0;
int _skipBottom = 0;
int _pressed = -1;
bool _appearing = false;
bool _toggling = false;
bool _small = false;
bool _over = false;
bool _low = false;
};
@ -113,6 +111,9 @@ enum class AttachSelectorResult {
AttachSelectorResult AttachSelectorToMenu(
not_null<Ui::PopupMenu*> menu,
QPoint desiredPosition,
not_null<HistoryItem*> item);
not_null<HistoryItem*> item,
Fn<void(ChosenReaction)> chosen,
Fn<void(FullMsgId)> showPremiumPromo,
IconFactory iconFactory);
} // namespace HistoryView::Reactions

View File

@ -0,0 +1,529 @@
/*
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/reactions/history_view_reactions_strip.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"
#include "main/main_session.h"
#include "styles/style_chat.h"
namespace HistoryView::Reactions {
namespace {
constexpr auto kSizeForDownscale = 96;
constexpr auto kEmojiCacheIndex = 0;
constexpr auto kHoverScaleDuration = crl::time(200);
constexpr auto kHoverScale = 1.24;
[[nodiscard]] int MainReactionSize() {
return style::ConvertScale(kSizeForDownscale);
}
[[nodiscard]] std::shared_ptr<Lottie::Icon> CreateIcon(
not_null<Data::DocumentMedia*> media,
int size,
int frame) {
Expects(media->loaded());
return std::make_shared<Lottie::Icon>(Lottie::IconDescriptor{
.path = media->owner()->filepath(true),
.json = media->bytes(),
.sizeOverride = QSize(size, size),
.frame = frame,
});
}
} // namespace
Strip::Strip(
QRect inner,
Fn<void()> update,
IconFactory iconFactory)
: _iconFactory(std::move(iconFactory))
, _inner(inner)
, _finalSize(st::reactionCornerImage)
, _update(std::move(update)) {
}
void Strip::applyList(
const std::vector<not_null<const Data::Reaction*>> &list,
AddedButton button) {
if (_button == button
&& ranges::equal(
ranges::make_subrange(
begin(_icons),
(begin(_icons)
+ _icons.size()
- (_button == AddedButton::None ? 0 : 1))),
list,
ranges::equal_to(),
&ReactionIcons::id,
&Data::Reaction::id)) {
return;
}
const auto selected = _selectedIcon;
setSelected(-1);
_icons.clear();
for (const auto &reaction : list) {
_icons.push_back({
.id = reaction->id,
.appearAnimation = reaction->appearAnimation,
.selectAnimation = reaction->selectAnimation,
});
}
_button = button;
if (_button != AddedButton::None) {
_icons.push_back({ .added = _button });
}
setSelected((selected < _icons.size()) ? selected : -1);
resolveMainReactionIcon();
}
void Strip::paint(
QPainter &p,
QPoint position,
QPoint shift,
QRect clip,
float64 scale,
bool hiding) {
const auto skip = st::reactionAppearStartSkip;
const auto animationRect = clip.marginsRemoved({ 0, skip, 0, skip });
PainterHighQualityEnabler hq(p);
const auto finalSize = st::reactionCornerImage;
const auto hoveredSize = int(base::SafeRound(finalSize * kHoverScale));
const auto basicTargetForScale = [&](int size, float64 scale) {
const auto remove = size * (1. - scale) / 2.;
return QRectF(QRect(
_inner.x() + (_inner.width() - size) / 2,
_inner.y() + (_inner.height() - size) / 2,
size,
size
)).marginsRemoved({ remove, remove, remove, remove });
};
const auto basicTarget = basicTargetForScale(finalSize, scale);
const auto countTarget = [&](const ReactionIcons &icon) {
const auto selectScale = icon.selectedScale.value(
icon.selected ? kHoverScale : 1.);
if (selectScale == 1.) {
return basicTarget;
}
const auto finalScale = scale * selectScale;
return (finalScale <= 1.)
? basicTargetForScale(finalSize, finalScale)
: basicTargetForScale(hoveredSize, finalScale / kHoverScale);
};
for (auto &icon : _icons) {
const auto target = countTarget(icon).translated(position);
position += shift;
const auto paintFrame = [&](not_null<Lottie::Icon*> animation) {
const auto size = int(std::floor(target.width() + 0.01));
const auto frame = animation->frame({ size, size }, _update);
p.drawImage(target, frame.image);
};
if (!target.intersects(clip)) {
if (!hiding) {
clearStateForHidden(icon);
}
} else if (icon.added == AddedButton::Premium) {
paintPremiumIcon(p, position - shift, target);
} else if (icon.added == AddedButton::Expand) {
paintExpandIcon(p, position - shift, target);
} else {
const auto appear = icon.appear.get();
if (!hiding
&& appear
&& !icon.appearAnimated
&& target.intersects(animationRect)) {
icon.appearAnimated = true;
appear->animate(_update, 0, appear->framesCount() - 1);
}
if (appear && appear->animating()) {
paintFrame(appear);
} else if (const auto select = icon.select.get()) {
paintFrame(select);
}
}
if (!hiding) {
clearStateForSelectFinished(icon);
}
}
}
bool Strip::empty() const {
return _icons.empty();
}
int Strip::count() const {
return _icons.size();
}
bool Strip::onlyAddedButton() const {
return (_icons.size() == 1)
&& (_icons.front().added != AddedButton::None);
}
int Strip::fillChosenIconGetIndex(ChosenReaction &chosen) const {
const auto i = ranges::find(_icons, chosen.id, &ReactionIcons::id);
if (i == end(_icons)) {
return -1;
}
const auto &icon = *i;
if (const auto &appear = icon.appear; appear && appear->animating()) {
chosen.icon = CreateIcon(
icon.appearAnimation->activeMediaView().get(),
appear->width(),
appear->frameIndex());
} else if (const auto &select = icon.select) {
chosen.icon = CreateIcon(
icon.selectAnimation->activeMediaView().get(),
select->width(),
select->frameIndex());
}
return (i - begin(_icons));
}
void Strip::paintPremiumIcon(
QPainter &p,
QPoint position,
QRectF target) const {
const auto to = QRect(
_inner.x() + (_inner.width() - _finalSize) / 2,
_inner.y() + (_inner.height() - _finalSize) / 2,
_finalSize,
_finalSize
).translated(position);
const auto scale = target.width() / to.width();
if (scale != 1.) {
p.save();
p.translate(target.center());
p.scale(scale, scale);
p.translate(-target.center());
}
auto hq = PainterHighQualityEnabler(p);
st::reactionPremiumLocked.paintInCenter(p, to);
if (scale != 1.) {
p.restore();
}
}
void Strip::paintExpandIcon(
QPainter &p,
QPoint position,
QRectF target) const {
const auto to = QRect(
_inner.x() + (_inner.width() - _finalSize) / 2,
_inner.y() + (_inner.height() - _finalSize) / 2,
_finalSize,
_finalSize
).translated(position);
const auto scale = target.width() / to.width();
if (scale != 1.) {
p.save();
p.translate(target.center());
p.scale(scale, scale);
p.translate(-target.center());
}
auto hq = PainterHighQualityEnabler(p);
st::reactionExpandPanel.paintInCenter(p, to);
if (scale != 1.) {
p.restore();
}
}
void Strip::setSelected(int index) const {
const auto set = [&](int index, bool selected) {
if (index < 0 || index >= _icons.size()) {
return;
}
auto &icon = _icons[index];
if (icon.selected == selected) {
return;
}
icon.selected = selected;
icon.selectedScale.start(
_update,
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;
select->animate(_update, 0, select->framesCount() - 1);
}
}
};
if (_selectedIcon != index) {
set(_selectedIcon, false);
_selectedIcon = index;
}
set(index, true);
}
auto Strip::selected() const -> std::variant<AddedButton, ReactionId> {
if (_selectedIcon < 0 || _selectedIcon >= _icons.size()) {
return {};
}
const auto &icon = _icons[_selectedIcon];
if (icon.added != AddedButton::None) {
return icon.added;
}
return icon.id;
}
int Strip::computeOverSize() const {
return int(base::SafeRound(st::reactionCornerImage * kHoverScale));
}
void Strip::clearAppearAnimations(bool mainAppeared) {
auto main = mainAppeared;
for (auto &icon : _icons) {
if (!main) {
if (icon.selected) {
setSelected(-1);
}
icon.selectedScale.stop();
if (const auto select = icon.select.get()) {
select->jumpTo(0, nullptr);
}
icon.selectAnimated = false;
}
if (icon.appearAnimated != main) {
if (const auto appear = icon.appear.get()) {
appear->jumpTo(0, nullptr);
}
icon.appearAnimated = main;
}
main = false;
}
}
void Strip::clearStateForHidden(ReactionIcons &icon) {
if (const auto appear = icon.appear.get()) {
appear->jumpTo(0, nullptr);
}
if (icon.selected) {
setSelected(-1);
}
icon.appearAnimated = false;
icon.selectAnimated = false;
if (const auto select = icon.select.get()) {
select->jumpTo(0, nullptr);
}
icon.selectedScale.stop();
}
void Strip::clearStateForSelectFinished(ReactionIcons &icon) {
if (icon.selectAnimated
&& !icon.select->animating()
&& !icon.selected) {
icon.selectAnimated = false;
}
}
bool Strip::checkIconLoaded(ReactionDocument &entry) const {
if (!entry.media) {
return true;
} else if (!entry.media->loaded()) {
return false;
}
const auto size = (entry.media == _mainReactionMedia)
? MainReactionSize()
: _finalSize;
entry.icon = _iconFactory(entry.media.get(), size);
entry.media = nullptr;
return true;
}
void Strip::loadIcons() {
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();
if (!checkIconLoaded(entry) && !_loadCacheLifetime) {
document->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
checkIcons();
}, _loadCacheLifetime);
}
return entry.icon;
};
auto all = true;
for (auto &icon : _icons) {
if (icon.appearAnimation && !icon.appear) {
icon.appear = load(icon.appearAnimation);
if (!icon.appear) {
all = false;
}
}
if (icon.selectAnimation && !icon.select) {
icon.select = load(icon.selectAnimation);
if (!icon.select) {
all = false;
}
}
}
if (all && !_icons.empty() && _icons.front().appearAnimation) {
auto &data = _icons.front().appearAnimation->owner().reactions();
for (const auto &icon : _icons) {
data.preloadAnimationsFor(icon.id);
}
}
}
void Strip::checkIcons() {
auto all = true;
for (auto &[document, entry] : _loadCache) {
if (!checkIconLoaded(entry)) {
all = false;
}
}
if (all) {
_loadCacheLifetime.destroy();
loadIcons();
}
}
void Strip::resolveMainReactionIcon() {
if (_icons.empty() || onlyAddedButton()) {
_mainReactionMedia = nullptr;
_mainReactionLifetime.destroy();
return;
}
const auto main = _icons.front().selectAnimation;
Assert(main != nullptr);
_icons.front().appearAnimated = true;
if (_mainReactionMedia && _mainReactionMedia->owner() == main) {
if (!_mainReactionLifetime) {
loadIcons();
}
return;
}
_mainReactionMedia = main->createMediaView();
_mainReactionMedia->checkStickerLarge();
if (_mainReactionMedia->loaded()) {
_mainReactionLifetime.destroy();
setMainReactionIcon();
} else if (!_mainReactionLifetime) {
main->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _mainReactionMedia->loaded();
}) | rpl::take(1) | rpl::start_with_next([=] {
setMainReactionIcon();
}, _mainReactionLifetime);
}
}
void Strip::setMainReactionIcon() {
_mainReactionLifetime.destroy();
ranges::fill(_validEmoji, false);
loadIcons();
const auto i = _loadCache.find(_mainReactionMedia->owner());
if (i != end(_loadCache) && i->second.icon) {
const auto &icon = i->second.icon;
if (!icon->frameIndex() && icon->width() == MainReactionSize()) {
_mainReactionImage = i->second.icon->frame();
return;
}
}
_mainReactionImage = QImage();
_mainReactionIcon = DefaultIconFactory(
_mainReactionMedia.get(),
MainReactionSize());
}
bool Strip::onlyMainEmojiVisible() const {
if (_icons.empty()) {
return true;
}
const auto &icon = _icons.front();
if (icon.selected
|| icon.selectedScale.animating()
|| (icon.select && icon.select->animating())) {
return false;
}
icon.selectAnimated = false;
return true;
}
Ui::ImageSubrect Strip::validateEmoji(int frameIndex, float64 scale) {
const auto area = _inner.size();
const auto size = int(base::SafeRound(_finalSize * scale));
const auto result = Ui::ImageSubrect{
&_emojiParts,
Ui::RoundAreaWithShadow::FrameCacheRect(
frameIndex,
kEmojiCacheIndex,
area),
};
if (_validEmoji[frameIndex]) {
return result;
} else if (_emojiParts.isNull()) {
_emojiParts = Ui::RoundAreaWithShadow::PrepareFramesCache(area);
}
auto p = QPainter(result.image);
const auto ratio = style::DevicePixelRatio();
const auto position = result.rect.topLeft() / ratio;
p.setCompositionMode(QPainter::CompositionMode_Source);
p.fillRect(QRect(position, result.rect.size() / ratio), Qt::transparent);
if (_mainReactionImage.isNull()
&& _mainReactionIcon) {
_mainReactionImage = base::take(_mainReactionIcon)->frame();
}
if (!_mainReactionImage.isNull()) {
const auto target = QRect(
(_inner.width() - size) / 2,
(_inner.height() - size) / 2,
size,
size
).translated(position);
p.drawImage(target, _mainReactionImage.scaled(
target.size() * ratio,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation));
}
_validEmoji[frameIndex] = true;
return result;
}
IconFactory CachedIconFactory::createMethod() {
return [=](not_null<Data::DocumentMedia*> media, int size) {
const auto owned = media->owner()->createMediaView();
const auto i = _cache.find(owned);
return (i != end(_cache))
? i->second
: _cache.emplace(
owned,
DefaultIconFactory(media, size)).first->second;
};
}
std::shared_ptr<Lottie::Icon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,
int size) {
return CreateIcon(media, size, 0);
}
} // namespace HistoryView::Reactions

View File

@ -0,0 +1,156 @@
/*
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 "ui/effects/animations.h"
#include "ui/effects/round_area_with_shadow.h"
#include "data/data_message_reaction_id.h"
class HistoryItem;
namespace Data {
struct Reaction;
class DocumentMedia;
} // namespace Data
namespace Lottie {
class Icon;
} // namespace Lottie
namespace HistoryView::Reactions {
struct ChosenReaction {
FullMsgId context;
Data::ReactionId id;
std::shared_ptr<Lottie::Icon> icon;
QRect geometry;
explicit operator bool() const {
return context && !id.empty();
}
};
using IconFactory = Fn<std::shared_ptr<Lottie::Icon>(
not_null<Data::DocumentMedia*>,
int)>;
class Strip final {
public:
using ReactionId = Data::ReactionId;
Strip(QRect inner, Fn<void()> update, IconFactory iconFactory);
enum class AddedButton : uchar {
None,
Expand,
Premium,
};
void applyList(
const std::vector<not_null<const Data::Reaction*>> &list,
AddedButton button);
void paint(
QPainter &p,
QPoint position,
QPoint shift,
QRect clip,
float64 scale,
bool hiding);
[[nodiscard]] bool empty() const;
[[nodiscard]] int count() const;
void setSelected(int index) const;
[[nodiscard]] std::variant<AddedButton, ReactionId> selected() const;
[[nodiscard]] int computeOverSize() const;
void clearAppearAnimations(bool mainAppeared = true);
int fillChosenIconGetIndex(ChosenReaction &chosen) const;
[[nodiscard]] bool onlyAddedButton() const;
[[nodiscard]] bool onlyMainEmojiVisible() const;
Ui::ImageSubrect validateEmoji(int frameIndex, float64 scale);
private:
static constexpr auto kFramesCount
= Ui::RoundAreaWithShadow::kFramesCount;
using ReactionId = ::Data::ReactionId;
struct ReactionDocument {
std::shared_ptr<Data::DocumentMedia> media;
std::shared_ptr<Lottie::Icon> icon;
};
struct ReactionIcons {
ReactionId id;
DocumentData *appearAnimation = nullptr;
DocumentData *selectAnimation = nullptr;
std::shared_ptr<Lottie::Icon> appear;
std::shared_ptr<Lottie::Icon> select;
mutable Ui::Animations::Simple selectedScale;
AddedButton added = AddedButton::None;
bool appearAnimated = false;
mutable bool selected = false;
mutable bool selectAnimated = false;
};
void clearStateForHidden(ReactionIcons &icon);
void paintPremiumIcon(QPainter &p, QPoint position, QRectF target) const;
void paintExpandIcon(QPainter &p, QPoint position, QRectF target) const;
void clearStateForSelectFinished(ReactionIcons &icon);
[[nodiscard]] bool checkIconLoaded(ReactionDocument &entry) const;
void loadIcons();
void checkIcons();
void resolveMainReactionIcon();
void setMainReactionIcon();
const IconFactory _iconFactory;
const QRect _inner;
const int _finalSize = 0;
Fn<void()> _update;
std::vector<ReactionIcons> _icons;
AddedButton _button = AddedButton::None;
base::flat_map<not_null<DocumentData*>, ReactionDocument> _loadCache;
std::optional<ReactionIcons> _premiumIcon;
rpl::lifetime _loadCacheLifetime;
mutable int _selectedIcon = -1;
std::shared_ptr<Data::DocumentMedia> _mainReactionMedia;
std::shared_ptr<Lottie::Icon> _mainReactionIcon;
QImage _mainReactionImage;
rpl::lifetime _mainReactionLifetime;
QImage _emojiParts;
std::array<bool, kFramesCount> _validEmoji = { { false } };
};
class CachedIconFactory final {
public:
CachedIconFactory() = default;
CachedIconFactory(const CachedIconFactory &other) = delete;
CachedIconFactory &operator=(const CachedIconFactory &other) = delete;
[[nodiscard]] IconFactory createMethod();
private:
base::flat_map<
std::shared_ptr<Data::DocumentMedia>,
std::shared_ptr<Lottie::Icon>> _cache;
};
[[nodiscard]] std::shared_ptr<Lottie::Icon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,
int size);
} // namespace HistoryView

View File

@ -1067,6 +1067,10 @@ reactionPremiumLocked: icon{
{ "chat/reactions_premium_bg", historyPeerArchiveUserpicBg },
{ "chat/reactions_premium_star", historyPeerUserpicFg },
};
reactionExpandPanel: icon{
{ "chat/reactions_expand_bg", historyPeerArchiveUserpicBg },
{ "chat/reactions_expand_panel", historyPeerUserpicFg },
};
searchInChatMultiSelectItem: MultiSelectItem(defaultMultiSelectItem) {
maxWidth: 200px;

View File

@ -382,4 +382,15 @@ bool ShowReactPremiumError(
return true;
}
void ShowPremiumPromoBox(
not_null<SessionController*> controller,
not_null<HistoryItem*> item) {
const auto &list = controller->session().data().reactions().list(
Data::Reactions::Type::Active);
ShowPremiumPreviewBox(
controller,
PremiumPreview::Reactions,
ExtractDisabledReactions(item->history()->peer, list));
}
} // namespace Window

View File

@ -219,4 +219,8 @@ private:
not_null<HistoryItem*> item,
const Data::ReactionId &id);
void ShowPremiumPromoBox(
not_null<SessionController*> controller,
not_null<HistoryItem*> item);
} // namespace Window