Implement reactions selector above the menu.
|
@ -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
|
||||
|
|
BIN
Telegram/Resources/icons/chat/reactions_bubble.png
Normal file
After Width: | Height: | Size: 331 B |
BIN
Telegram/Resources/icons/chat/reactions_bubble@2x.png
Normal file
After Width: | Height: | Size: 531 B |
BIN
Telegram/Resources/icons/chat/reactions_bubble@3x.png
Normal file
After Width: | Height: | Size: 772 B |
BIN
Telegram/Resources/icons/chat/reactions_bubble_shadow.png
Normal file
After Width: | Height: | Size: 496 B |
BIN
Telegram/Resources/icons/chat/reactions_bubble_shadow@2x.png
Normal file
After Width: | Height: | Size: 967 B |
BIN
Telegram/Resources/icons/chat/reactions_bubble_shadow@3x.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Telegram/Resources/icons/chat/reactions_expand_bg.png
Normal file
After Width: | Height: | Size: 418 B |
BIN
Telegram/Resources/icons/chat/reactions_expand_bg@2x.png
Normal file
After Width: | Height: | Size: 803 B |
BIN
Telegram/Resources/icons/chat/reactions_expand_bg@3x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/chat/reactions_expand_panel.png
Normal file
After Width: | Height: | Size: 244 B |
BIN
Telegram/Resources/icons/chat/reactions_expand_panel@2x.png
Normal file
After Width: | Height: | Size: 337 B |
BIN
Telegram/Resources/icons/chat/reactions_expand_panel@3x.png
Normal file
After Width: | Height: | Size: 465 B |
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(); }) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ¶meters);
|
||||
|
||||
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
|
|
@ -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 ¶meters);
|
||||
|
||||
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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|