Implement custom reactions in stories.

This commit is contained in:
John Preston 2023-08-08 10:55:12 +02:00
parent 066dbfe8fc
commit 13f67d68c4
21 changed files with 744 additions and 192 deletions

View File

@ -226,8 +226,9 @@ struct StoryUpdate {
NewAdded = (1U << 2),
ViewsAdded = (1U << 3),
MarkRead = (1U << 4),
Reaction = (1U << 5),
LastUsedBit = (1U << 4),
LastUsedBit = (1U << 5),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View File

@ -381,7 +381,7 @@ void Reactions::preloadImageFor(const ReactionId &id) {
loadImage(set, document, !i->centerIcon);
} else if (!_waitingForList) {
_waitingForList = true;
refreshRecent();
refreshDefault();
}
}

View File

@ -870,6 +870,21 @@ void Stories::activateStealthMode(Fn<void()> done) {
}).send();
}
void Stories::sendReaction(FullStoryId id, Data::ReactionId reaction) {
if (const auto maybeStory = lookup(id)) {
const auto story = *maybeStory;
story->setReactionId(reaction);
const auto api = &session().api();
api->request(MTPstories_SendReaction(
MTP_flags(0),
story->peer()->asUser()->inputUser,
MTP_int(id.story),
ReactionToMTP(reaction)
)).send();
}
}
std::shared_ptr<HistoryItem> Stories::resolveItem(not_null<Story*> story) {
auto &items = _items[story->peer()->id];
auto i = items.find(story->id());

View File

@ -240,6 +240,8 @@ public:
[[nodiscard]] rpl::producer<StealthMode> stealthModeValue() const;
void activateStealthMode(Fn<void()> done = nullptr);
void sendReaction(FullStoryId id, Data::ReactionId reaction);
private:
struct Saved {
StoriesIds ids;

View File

@ -376,6 +376,17 @@ const TextWithEntities &Story::caption() const {
return unsupported() ? empty : _caption;
}
Data::ReactionId Story::sentReactionId() const {
return _sentReactionId;
}
void Story::setReactionId(Data::ReactionId id) {
if (_sentReactionId != id) {
_sentReactionId = id;
session().changes().storyUpdated(this, UpdateFlag::Reaction);
}
}
const std::vector<not_null<PeerData*>> &Story::recentViewers() const {
return _recentViewers;
}
@ -458,6 +469,9 @@ void Story::applyFields(
bool initial) {
_lastUpdateTime = now;
const auto reaction = data.vsent_reaction()
? Data::ReactionFromMTP(*data.vsent_reaction())
: Data::ReactionId();
const auto pinned = data.is_pinned();
const auto edited = data.is_edited();
const auto privacy = data.is_public()
@ -512,6 +526,7 @@ void Story::applyFields(
|| (_views.reactions != reactions)
|| (_recentViewers != viewers);
const auto locationsChanged = (_locations != locations);
const auto reactionChanged = (_sentReactionId != reaction);
_privacyPublic = (privacy == StoryPrivacy::Public);
_privacyCloseFriends = (privacy == StoryPrivacy::CloseFriends);
@ -536,15 +551,19 @@ void Story::applyFields(
if (locationsChanged) {
_locations = std::move(locations);
}
if (reactionChanged) {
_sentReactionId = reaction;
}
const auto changed = editedChanged
|| captionChanged
|| mediaChanged
|| locationsChanged;
if (!initial && (changed || viewsChanged)) {
if (!initial && (changed || viewsChanged || reactionChanged)) {
_peer->session().changes().storyUpdated(this, UpdateFlag()
| (changed ? UpdateFlag::Edited : UpdateFlag())
| (viewsChanged ? UpdateFlag::ViewsAdded : UpdateFlag()));
| (viewsChanged ? UpdateFlag::ViewsAdded : UpdateFlag())
| (reactionChanged ? UpdateFlag::Reaction : UpdateFlag()));
}
if (!initial && (captionChanged || mediaChanged)) {
if (const auto item = _peer->owner().stories().lookupItem(this)) {

View File

@ -146,6 +146,9 @@ public:
void setCaption(TextWithEntities &&caption);
[[nodiscard]] const TextWithEntities &caption() const;
[[nodiscard]] Data::ReactionId sentReactionId() const;
void setReactionId(Data::ReactionId id);
[[nodiscard]] auto recentViewers() const
-> const std::vector<not_null<PeerData*>> &;
[[nodiscard]] const StoryViews &viewsList() const;
@ -170,6 +173,7 @@ private:
const StoryId _id = 0;
const not_null<PeerData*> _peer;
Data::ReactionId _sentReactionId;
StoryMedia _media;
TextWithEntities _caption;
std::vector<not_null<PeerData*>> _recentViewers;

View File

@ -1241,9 +1241,12 @@ bool ComposeControls::focus() {
return true;
}
bool ComposeControls::focused() const {
return Ui::InFocusChain(_wrap.get());
}
rpl::producer<bool> ComposeControls::focusedValue() const {
return rpl::single(Ui::InFocusChain(_wrap.get()))
| rpl::then(_focusChanges.events());
return rpl::single(focused()) | rpl::then(_focusChanges.events());
}
rpl::producer<bool> ComposeControls::tabbedPanelShownValue() const {
@ -3022,7 +3025,7 @@ bool ComposeControls::handleCancelRequest() {
}
void ComposeControls::tryProcessKeyInput(not_null<QKeyEvent*> e) {
if (_field->isVisible()) {
if (_field->isVisible() && !e->text().isEmpty()) {
_field->setFocusFast();
QCoreApplication::sendEvent(_field->rawTextEdit(), e);
}
@ -3158,7 +3161,7 @@ rpl::producer<bool> ComposeControls::fieldMenuShownValue() const {
return _field->menuShownValue();
}
not_null<QWidget*> ComposeControls::likeAnimationTarget() const {
not_null<Ui::RpWidget*> ComposeControls::likeAnimationTarget() const {
Expects(_like != nullptr);
return _like;

View File

@ -147,6 +147,7 @@ public:
[[nodiscard]] int heightCurrent() const;
bool focus();
[[nodiscard]] bool focused() const;
[[nodiscard]] rpl::producer<bool> focusedValue() const;
[[nodiscard]] rpl::producer<bool> tabbedPanelShownValue() const;
[[nodiscard]] rpl::producer<> cancelRequests() const;
@ -222,7 +223,7 @@ public:
[[nodiscard]] rpl::producer<bool> recordingActiveValue() const;
[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
[[nodiscard]] rpl::producer<bool> fieldMenuShownValue() const;
[[nodiscard]] not_null<QWidget*> likeAnimationTarget() const;
[[nodiscard]] not_null<Ui::RpWidget*> likeAnimationTarget() const;
void applyCloudDraft();
void applyDraft(

View File

@ -328,6 +328,10 @@ void Selector::updateShowState(
update();
}
int Selector::countAppearedWidth(float64 progress) const {
return anim::interpolate(_skipx * 2 + _size, _inner.width(), progress);
}
void Selector::paintAppearing(QPainter &p) {
Expects(_strip != nullptr);
@ -340,10 +344,7 @@ void Selector::paintAppearing(QPainter &p) {
_paintBuffer.fill(_st.bg->c);
auto q = QPainter(&_paintBuffer);
const auto extents = extentsForShadow();
const auto appearedWidth = anim::interpolate(
_skipx * 2 + _size,
_inner.width(),
_appearProgress);
const auto appearedWidth = countAppearedWidth(_appearProgress);
const auto fullWidth = _inner.x() + appearedWidth + extents.right();
const auto size = QSize(fullWidth, _outer.height());

View File

@ -63,6 +63,7 @@ public:
[[nodiscard]] QMargins extentsForShadow() const;
[[nodiscard]] int extendTopForCategories() const;
[[nodiscard]] int minimalHeight() const;
[[nodiscard]] int countAppearedWidth(float64 progress) const;
void setSpecialExpandTopSkip(int skip);
void initGeometry(int innerTop);
void beforeDestroy();

View File

@ -7,19 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "media/stories/media_stories_controller.h"
#include "base/timer.h"
#include "base/power_save_blocker.h"
#include "base/qt_signal_producer.h"
#include "base/unixtime.h"
#include "boxes/peers/prepare_short_info_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/update_checker.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_user.h"
@ -40,22 +38,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/audio/media_audio.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/report_box.h"
#include "ui/effects/emoji_fly_animation.h"
#include "ui/effects/message_sending_animation_common.h"
#include "ui/effects/reaction_fly_animation.h"
#include "ui/layers/box_content.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/round_rect.h"
#include "ui/rp_widget.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_chat_helpers.h" // defaultReportBox
#include "styles/style_media_view.h"
#include "styles/style_widgets.h"
#include "styles/style_boxes.h" // UserpicButton
#include <QtGui/QWindow>
@ -114,10 +105,6 @@ struct SameDayRange {
return result;
}
[[nodiscard]] Data::ReactionId HeartReactionId() {
return { QString() + QChar(10084) };
}
[[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) {
if (std::abs(angle) < 1.) {
return point;
@ -294,7 +281,7 @@ Controller::Controller(not_null<Delegate*> delegate)
rpl::combine(
_replyArea->activeValue(),
_reactions->expandedValue(),
_reactions->activeValue(),
_1 || _2
) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool active) {
@ -302,38 +289,16 @@ Controller::Controller(not_null<Delegate*> delegate)
updateContentFaded();
}, _lifetime);
_replyArea->focusedValue(
) | rpl::start_with_next([=](bool focused) {
_replyFocused = focused;
if (!_replyFocused) {
_reactions->hideIfCollapsed();
} else if (!_hasSendText) {
_reactions->show();
}
}, _lifetime);
_replyArea->hasSendTextValue(
) | rpl::start_with_next([=](bool has) {
_hasSendText = has;
if (_replyFocused) {
if (_hasSendText) {
_reactions->hide();
} else {
_reactions->show();
}
}
}, _lifetime);
_reactions->setReplyFieldState(
_replyArea->focusedValue(),
_replyArea->hasSendTextValue());
if (const auto like = _replyArea->likeAnimationTarget()) {
_reactions->attachToReactionButton(like);
}
_reactions->chosen(
) | rpl::start_with_next([=](HistoryView::Reactions::ChosenReaction id) {
startReactionAnimation({
.id = id.id,
.flyIcon = id.icon,
.flyFrom = _wrap->mapFromGlobal(id.globalGeometry),
.scaleOutDuration = st::fadeWrapDuration * 2,
}, _wrap.get());
_replyArea->sendReaction(id.id);
unfocusReply();
) | rpl::start_with_next([=](Reactions::Chosen chosen) {
reactionChosen(chosen.mode, chosen.reaction);
}, _lifetime);
_delegate->storiesLayerShown(
@ -624,23 +589,17 @@ bool Controller::skipCaption() const {
return _captionFullView != nullptr;
}
bool Controller::liked() const {
return _liked.current();
void Controller::toggleLiked() {
_reactions->toggleLiked();
}
rpl::producer<bool> Controller::likedValue() const {
return _liked.value();
}
void Controller::toggleLiked(bool liked) {
_liked = liked;
if (liked) {
startReactionAnimation({
.id = HeartReactionId(),
.scaleOutDuration = st::fadeWrapDuration * 2,
.effectOnly = true,
}, _replyArea->likeAnimationTarget());
void Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) {
if (mode == ReactionsMode::Message) {
_replyArea->sendReaction(chosen.id);
} else if (const auto user = shownUser()) {
user->owner().stories().sendReaction(_shown, chosen.id);
}
unfocusReply();
}
void Controller::showFullCaption() {
@ -902,14 +861,15 @@ void Controller::show(
_viewed = false;
invalidate_weak_ptrs(&_viewsLoadGuard);
_reactions->hide();
if (_replyFocused) {
if (_replyArea->focused()) {
unfocusReply();
}
_replyArea->show({
.user = unsupported ? nullptr : user,
.id = story->id(),
});
}, _reactions->likedValue());
_recentViews->show({
.list = story->recentViewers(),
.reactions = story->reactions(),
@ -949,7 +909,8 @@ bool Controller::changeShown(Data::Story *story) {
story,
Data::Stories::Polling::Viewer);
}
_liked = false;
_reactions->showLikeFrom(story);
const auto &locations = story
? story->locations()
: std::vector<Data::StoryLocation>();
@ -1099,8 +1060,7 @@ void Controller::ready() {
}
_started = true;
updatePlayingAllowed();
uiShow()->session().data().reactions().preloadAnimationsFor(
HeartReactionId());
_reactions->ready();
}
void Controller::updateVideoPlayback(const Player::TrackState &state) {
@ -1291,7 +1251,7 @@ void Controller::contentPressed(bool pressed) {
_captionFullView->close();
}
if (pressed) {
_reactions->collapse();
_reactions->outsidePressed();
}
}
@ -1607,28 +1567,6 @@ void Controller::updatePowerSaveBlocker(const Player::TrackState &state) {
[=] { return _wrap->window()->windowHandle(); });
}
void Controller::startReactionAnimation(
Ui::ReactionFlyAnimationArgs args,
not_null<QWidget*> target) {
Expects(shown());
_reactionAnimation = std::make_unique<Ui::EmojiFlyAnimation>(
_wrap,
&shownUser()->owner().reactions(),
std::move(args),
[=] { _reactionAnimation->repaint(); },
Data::CustomEmojiSizeTag::Isolated);
const auto layer = _reactionAnimation->layer();
_wrap->paintRequest() | rpl::start_with_next([=] {
if (!_reactionAnimation->paintBadgeFrame(target)) {
InvokeQueued(layer, [=] {
_reactionAnimation = nullptr;
_wrap->update();
});
}
}, layer->lifetime());
}
Ui::Toast::Config PrepareTogglePinnedToast(int count, bool pinned) {
return {
.text = (pinned

View File

@ -26,17 +26,16 @@ struct FileChosen;
namespace Data {
struct FileOrigin;
struct ReactionId;
class DocumentMedia;
} // namespace Data
namespace HistoryView::Reactions {
class CachedIconFactory;
struct ChosenReaction;
} // namespace HistoryView::Reactions
namespace Ui {
class RpWidget;
struct ReactionFlyAnimationArgs;
class EmojiFlyAnimation;
class BoxContent;
} // namespace Ui
@ -66,6 +65,7 @@ struct SiblingView;
enum class SiblingType;
struct ContentLayout;
class CaptionFullView;
enum class ReactionsMode;
enum class HeaderLayout {
Normal,
@ -118,9 +118,7 @@ public:
[[nodiscard]] Data::FileOrigin fileOrigin() const;
[[nodiscard]] TextWithEntities captionText() const;
[[nodiscard]] bool skipCaption() const;
[[nodiscard]] bool liked() const;
[[nodiscard]] rpl::producer<bool> likedValue() const;
void toggleLiked(bool liked);
void toggleLiked();
void showFullCaption();
void captionClosing();
void captionClosed();
@ -172,6 +170,9 @@ public:
[[nodiscard]] rpl::lifetime &lifetime();
private:
class PhotoPlayback;
class Unsupported;
using ChosenReaction = HistoryView::Reactions::ChosenReaction;
struct StoriesList {
not_null<UserData*> user;
Data::StoriesIds ids;
@ -194,8 +195,6 @@ private:
float64 rotation = 0.;
ClickHandlerPtr handler;
};
class PhotoPlayback;
class Unsupported;
void initLayout();
bool changeShown(Data::Story *story);
@ -238,9 +237,7 @@ private:
const std::vector<Data::StoriesSourceInfo> &lists,
int index);
void startReactionAnimation(
Ui::ReactionFlyAnimationArgs from,
not_null<QWidget*> target);
void reactionChosen(ReactionsMode mode, ChosenReaction chosen);
const not_null<Delegate*> _delegate;
@ -260,9 +257,7 @@ private:
bool _contentFaded = false;
bool _windowActive = false;
bool _replyFocused = false;
bool _replyActive = false;
bool _hasSendText = false;
bool _layerShown = false;
bool _menuShown = false;
bool _tooltipShown = false;
@ -273,7 +268,6 @@ private:
Data::StoriesContext _context;
std::optional<Data::StoriesSource> _source;
std::optional<StoriesList> _list;
rpl::variable<bool> _liked;
FullStoryId _waitingForId;
int _waitingForDelta = 0;
int _index = 0;
@ -297,7 +291,6 @@ private:
std::unique_ptr<Sibling> _siblingRight;
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
std::unique_ptr<Ui::EmojiFlyAnimation> _reactionAnimation;
Main::Session *_session = nullptr;
rpl::lifetime _sessionLifetime;

View File

@ -7,13 +7,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "media/stories/media_stories_reactions.h"
#include "base/event_filter.h"
#include "boxes/premium_preview_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "history/view/reactions/history_view_reactions_selector.h"
#include "main/main_session.h"
#include "media/stories/media_stories_controller.h"
#include "ui/effects/emoji_fly_animation.h"
#include "ui/effects/reaction_fly_animation.h"
#include "ui/animated_icon.h"
#include "ui/painter.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_media_view.h"
#include "styles/style_widgets.h"
@ -21,6 +29,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Media::Stories {
namespace {
constexpr auto kReactionScaleOutTarget = 0.7;
constexpr auto kReactionScaleOutDuration = crl::time(1000);
constexpr auto kMessageReactionScaleOutDuration = crl::time(400);
[[nodiscard]] Data::ReactionId HeartReactionId() {
return { QString() + QChar(10084) };
}
[[nodiscard]] Data::PossibleItemReactionsRef LookupPossibleReactions(
not_null<Main::Session*> session) {
auto result = Data::PossibleItemReactionsRef();
@ -51,7 +67,50 @@ namespace {
} // namespace
struct Reactions::Hiding {
class Reactions::Panel final {
public:
explicit Panel(not_null<Controller*> controller);
~Panel();
[[nodiscard]] rpl::producer<bool> expandedValue() const {
return _expanded.value();
}
[[nodiscard]] rpl::producer<bool> shownValue() const {
return _shown.value();
}
[[nodiscard]] rpl::producer<Chosen> chosen() const;
void show(Mode mode);
void hide(Mode mode);
void hideIfCollapsed(Mode mode);
void collapse(Mode mode);
void attachToReactionButton(not_null<Ui::RpWidget*> button);
private:
struct Hiding;
void create();
void updateShowState();
void fadeOutSelector();
void startAnimation();
const not_null<Controller*> _controller;
std::unique_ptr<Ui::RpWidget> _parent;
std::unique_ptr<HistoryView::Reactions::Selector> _selector;
std::vector<std::unique_ptr<Hiding>> _hiding;
rpl::event_stream<Chosen> _chosen;
Ui::Animations::Simple _showing;
rpl::variable<float64> _shownValue;
rpl::variable<bool> _expanded;
rpl::variable<Mode> _mode;
rpl::variable<bool> _shown = false;
};
struct Reactions::Panel::Hiding {
explicit Hiding(not_null<QWidget*> parent) : widget(parent) {
}
@ -60,16 +119,24 @@ struct Reactions::Hiding {
QImage frame;
};
Reactions::Reactions(not_null<Controller*> controller)
Reactions::Panel::Panel(not_null<Controller*> controller)
: _controller(controller) {
}
Reactions::~Reactions() = default;
Reactions::Panel::~Panel() = default;
void Reactions::show() {
if (_shown) {
auto Reactions::Panel::chosen() const -> rpl::producer<Chosen> {
return _chosen.events();
}
void Reactions::Panel::show(Mode mode) {
const auto was = _mode.current();
if (_shown.current() && was == mode) {
return;
} else if (_shown.current()) {
hide(was);
}
_mode = mode;
create();
if (!_selector) {
return;
@ -82,8 +149,8 @@ void Reactions::show() {
_parent->show();
}
void Reactions::hide() {
if (!_selector) {
void Reactions::Panel::hide(Mode mode) {
if (!_selector || _mode.current() != mode) {
return;
}
_selector->beforeDestroy();
@ -97,20 +164,32 @@ void Reactions::hide() {
_parent = nullptr;
}
void Reactions::hideIfCollapsed() {
if (!_expanded.current()) {
hide();
void Reactions::Panel::hideIfCollapsed(Mode mode) {
if (!_expanded.current() && _mode.current() == mode) {
hide(mode);
}
}
void Reactions::collapse() {
if (_expanded.current()) {
hide();
show();
void Reactions::Panel::collapse(Mode mode) {
if (_expanded.current() && _mode.current() == mode) {
hide(mode);
show(mode);
}
}
void Reactions::create() {
void Reactions::Panel::attachToReactionButton(not_null<Ui::RpWidget*> button) {
base::install_event_filter(button, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::ContextMenu && !button->isHidden()) {
show(Reactions::Mode::Reaction);
return base::EventFilterResult::Cancel;
} else if (e->type() == QEvent::Hide) {
hide(Reactions::Mode::Reaction);
}
return base::EventFilterResult::Continue;
});
}
void Reactions::Panel::create() {
auto reactions = LookupPossibleReactions(
&_controller->uiShow()->session());
if (reactions.recent.empty() && !reactions.morePremiumAvailable) {
@ -119,13 +198,19 @@ void Reactions::create() {
_parent = std::make_unique<Ui::RpWidget>(_controller->wrap().get());
_parent->show();
const auto mode = _mode.current();
_parent->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
const auto event = static_cast<QMouseEvent*>(e.get());
if (event->button() == Qt::LeftButton) {
if (!_selector
|| !_selector->geometry().contains(event->pos())) {
collapse();
if (mode == Mode::Message) {
collapse(mode);
} else {
hide(mode);
}
}
}
}
@ -137,17 +222,17 @@ void Reactions::create() {
_controller->uiShow(),
std::move(reactions),
_controller->cachedReactionIconFactory().createMethod(),
[=](bool fast) { hide(); });
[=](bool fast) { hide(mode); });
_selector->chosen(
) | rpl::start_with_next([=](
HistoryView::Reactions::ChosenReaction reaction) {
_chosen.fire_copy(reaction);
hide();
_chosen.fire({ .reaction = reaction, .mode = mode });
hide(mode);
}, _selector->lifetime());
_selector->premiumPromoChosen() | rpl::start_with_next([=] {
hide();
hide(mode);
ShowPremiumPreviewBox(
_controller->uiShow(),
PremiumPreview::InfiniteReactions);
@ -165,13 +250,23 @@ void Reactions::create() {
_controller->layoutValue(),
_shownValue.value()
) | rpl::start_with_next([=](const Layout &layout, float64 shown) {
const auto shift = int(base::SafeRound((full / 2.) * shown));
_parent->setGeometry(QRect(
layout.reactions.x() + layout.reactions.width() / 2 - shift,
layout.reactions.y(),
full,
layout.reactions.height()));
const auto innerTop = layout.reactions.height()
const auto width = extents.left()
+ _selector->countAppearedWidth(shown)
+ extents.right();
const auto height = layout.reactions.height();
const auto shift = (width / 2);
const auto right = (mode == Mode::Message)
? (layout.reactions.x() + layout.reactions.width() / 2 + shift)
: (layout.controlsBottomPosition.x()
+ layout.controlsWidth
- st::storiesLikeReactionsPosition.x());
const auto top = (mode == Mode::Message)
? layout.reactions.y()
: (layout.controlsBottomPosition.y()
- height
- st::storiesLikeReactionsPosition.y());
_parent->setGeometry(QRect((right - width), top, full, height));
const auto innerTop = height
- st::storiesReactionsBottomSkip
- st::reactStripHeight;
const auto maxAdded = innerTop - extents.top() - categoriesTop;
@ -186,11 +281,15 @@ void Reactions::create() {
}, _selector->lifetime());
_selector->escapes() | rpl::start_with_next([=] {
collapse();
if (mode == Mode::Message) {
collapse(mode);
} else {
hide(mode);
}
}, _selector->lifetime());
}
void Reactions::fadeOutSelector() {
void Reactions::Panel::fadeOutSelector() {
const auto wrap = _controller->wrap().get();
const auto geometry = Ui::MapFrom(
wrap,
@ -226,8 +325,8 @@ void Reactions::fadeOutSelector() {
});
}
void Reactions::updateShowState() {
const auto progress = _showing.value(_shown ? 1. : 0.);
void Reactions::Panel::updateShowState() {
const auto progress = _showing.value(_shown.current() ? 1. : 0.);
const auto opacity = 1.;
const auto appearing = _showing.animating();
const auto toggling = false;
@ -235,4 +334,355 @@ void Reactions::updateShowState() {
_selector->updateShowState(progress, opacity, appearing, toggling);
}
Reactions::Reactions(not_null<Controller*> controller)
: _controller(controller)
, _panel(std::make_unique<Panel>(_controller)) {
_panel->chosen() | rpl::start_with_next([=](Chosen &&chosen) {
animateAndProcess(std::move(chosen));
}, _lifetime);
}
Reactions::~Reactions() = default;
rpl::producer<bool> Reactions::activeValue() const {
using namespace rpl::mappers;
return rpl::combine(
_panel->expandedValue(),
_panel->shownValue(),
_1 || _2);
}
auto Reactions::chosen() const -> rpl::producer<Chosen> {
return _chosen.events();
}
void Reactions::setReplyFieldState(
rpl::producer<bool> focused,
rpl::producer<bool> hasSendText) {
std::move(
focused
) | rpl::start_with_next([=](bool focused) {
_replyFocused = focused;
if (!_replyFocused) {
_panel->hideIfCollapsed(Reactions::Mode::Message);
} else if (!_hasSendText) {
_panel->show(Reactions::Mode::Message);
}
}, _lifetime);
std::move(
hasSendText
) | rpl::start_with_next([=](bool has) {
_hasSendText = has;
if (_replyFocused) {
if (_hasSendText) {
_panel->hide(Reactions::Mode::Message);
} else {
_panel->show(Reactions::Mode::Message);
}
}
}, _lifetime);
}
void Reactions::attachToReactionButton(not_null<Ui::RpWidget*> button) {
_likeButton = button;
_panel->attachToReactionButton(button);
}
Data::ReactionId Reactions::liked() const {
return _liked.current();
}
rpl::producer<Data::ReactionId> Reactions::likedValue() const {
return _liked.value();
}
void Reactions::showLikeFrom(Data::Story *story) {
setLikedIdFrom(story);
if (!story) {
_likeFromLifetime.destroy();
return;
}
_likeFromLifetime = story->session().changes().storyUpdates(
story,
Data::StoryUpdate::Flag::Reaction
) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
setLikedIdFrom(update.story);
});
}
void Reactions::hide() {
_panel->hide(Reactions::Mode::Message);
_panel->hide(Reactions::Mode::Reaction);
}
void Reactions::outsidePressed() {
_panel->hide(Reactions::Mode::Reaction);
_panel->collapse(Reactions::Mode::Message);
}
void Reactions::toggleLiked() {
const auto liked = !_liked.current().empty();
const auto now = liked ? Data::ReactionId() : HeartReactionId();
if (_liked.current() != now) {
animateAndProcess({ { .id = now }, ReactionsMode::Reaction });
}
}
void Reactions::ready() {
if (const auto story = _controller->story()) {
story->owner().reactions().preloadAnimationsFor(HeartReactionId());
}
}
void Reactions::animateAndProcess(Chosen &&chosen) {
const auto like = (chosen.mode == Mode::Reaction);
const auto wrap = _controller->wrap();
const auto target = like ? _likeButton : wrap.get();
const auto story = _controller->story();
if (!story || !target) {
return;
}
auto done = like
? setLikedIdIconInit(&story->owner(), chosen.reaction.id)
: Fn<void(Ui::ReactionFlyCenter)>();
const auto scaleOutDuration = like
? kReactionScaleOutDuration
: kMessageReactionScaleOutDuration;
const auto scaleOutTarget = like ? kReactionScaleOutTarget : 0.;
if (!chosen.reaction.id.empty()) {
startReactionAnimation({
.id = chosen.reaction.id,
.flyIcon = chosen.reaction.icon,
.flyFrom = (chosen.reaction.globalGeometry.isEmpty()
? QRect()
: wrap->mapFromGlobal(chosen.reaction.globalGeometry)),
.scaleOutDuration = scaleOutDuration,
.scaleOutTarget = scaleOutTarget,
}, target, std::move(done));
}
_chosen.fire(std::move(chosen));
}
void Reactions::assignLikedId(Data::ReactionId id) {
invalidate_weak_ptrs(&_likeIconGuard);
_likeIcon = nullptr;
_liked = id;
}
Fn<void(Ui::ReactionFlyCenter)> Reactions::setLikedIdIconInit(
not_null<Data::Session*> owner,
Data::ReactionId id,
bool force) {
if (_liked.current() != id) {
_likeIconMedia = nullptr;
} else if (!force) {
return nullptr;
}
assignLikedId(id);
if (id.empty() || !_likeButton) {
return nullptr;
}
return crl::guard(&_likeIconGuard, [=](Ui::ReactionFlyCenter center) {
if (!id.custom() && !center.icon && !_likeIconMedia) {
waitForLikeIcon(owner, id);
} else {
initLikeIcon(owner, id, std::move(center));
}
});
}
void Reactions::initLikeIcon(
not_null<Data::Session*> owner,
Data::ReactionId id,
Ui::ReactionFlyCenter center) {
Expects(_likeButton != nullptr);
_likeIcon = std::make_unique<Ui::RpWidget>(_likeButton);
const auto icon = _likeIcon.get();
icon->show();
_likeButton->sizeValue() | rpl::start_with_next([=](QSize size) {
icon->setGeometry(QRect(QPoint(), size));
}, icon->lifetime());
if (!id.custom() && !center.icon) {
return;
}
struct State {
Ui::ReactionFlyCenter center;
QImage cache;
};
const auto fly = icon->lifetime().make_state<State>(State{
.center = std::move(center),
});
if (const auto customId = id.custom()) {
auto withCorrectCallback = owner->customEmojiManager().create(
customId,
[=] { icon->update(); },
Data::CustomEmojiSizeTag::Isolated);
[[maybe_unused]] const auto load = withCorrectCallback->ready();
fly->center.custom = std::move(withCorrectCallback);
fly->center.icon = nullptr;
} else {
fly->center.icon->jumpToStart(nullptr);
fly->center.custom = nullptr;
}
const auto paintNonCached = [=](QPainter &p) {
auto hq = PainterHighQualityEnabler(p);
const auto size = fly->center.size;
const auto target = QRect(
(icon->width() - size) / 2,
(icon->height() - size) / 2,
size,
size);
const auto scale = fly->center.scale;
if (scale < 1.) {
const auto shift = QRectF(target).center();
p.translate(shift);
p.scale(scale, scale);
p.translate(-shift);
}
const auto multiplier = fly->center.centerSizeMultiplier;
const auto inner = int(base::SafeRound(size * multiplier));
if (const auto icon = fly->center.icon.get()) {
const auto rect = QRect(
target.x() + (target.width() - inner) / 2,
target.y() + (target.height() - inner) / 2,
inner,
inner);
p.drawImage(rect, icon->frame(st::windowFg->c));
} else {
const auto customSize = fly->center.customSize;
const auto scaled = (inner != customSize);
fly->center.custom->paint(p, {
.textColor = st::windowFg->c,
.size = { customSize, customSize },
.now = crl::now(),
.scale = (scaled ? (inner / float64(customSize)) : 1.),
.position = QPoint(
target.x() + (target.width() - customSize) / 2,
target.y() + (target.height() - customSize) / 2),
.scaled = scaled,
});
}
};
icon->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(icon);
if (!fly->cache.isNull()) {
p.drawImage(0, 0, fly->cache);
} else if (fly->center.icon
|| fly->center.custom->readyInDefaultState()) {
const auto ratio = style::DevicePixelRatio();
fly->cache = QImage(
icon->size() * ratio,
QImage::Format_ARGB32_Premultiplied);
fly->cache.setDevicePixelRatio(ratio);
fly->cache.fill(Qt::transparent);
auto q = QPainter(&fly->cache);
paintNonCached(q);
q.end();
fly->center.icon = nullptr;
fly->center.custom = nullptr;
p.drawImage(0, 0, fly->cache);
} else {
paintNonCached(p);
}
}, icon->lifetime());
}
void Reactions::waitForLikeIcon(
not_null<Data::Session*> owner,
Data::ReactionId id) {
_likeIconWaitLifetime = rpl::single(
rpl::empty
) | rpl::then(
owner->reactions().defaultUpdates()
) | rpl::map([=]() -> rpl::producer<bool> {
const auto &list = owner->reactions().list(
Data::Reactions::Type::All);
const auto i = ranges::find(list, id, &Data::Reaction::id);
if (i == end(list)) {
return rpl::single(false);
}
const auto document = i->centerIcon
? not_null(i->centerIcon)
: i->selectAnimation;
_likeIconMedia = document->createMediaView();
_likeIconMedia->checkStickerLarge();
return rpl::single(
rpl::empty
) | rpl::then(
document->session().downloaderTaskFinished()
) | rpl::map([=] {
return _likeIconMedia->loaded();
});
}) | rpl::flatten_latest(
) | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next([=] {
setLikedId(owner, id, true);
crl::on_main(&_likeIconGuard, [=] {
_likeIconMedia = nullptr;
_likeIconWaitLifetime.destroy();
});
});
}
void Reactions::setLikedIdFrom(Data::Story *story) {
if (!story) {
assignLikedId({});
} else {
setLikedId(&story->owner(), story->sentReactionId());
}
}
void Reactions::setLikedId(
not_null<Data::Session*> owner,
Data::ReactionId id,
bool force) {
if (const auto done = setLikedIdIconInit(owner, id, force)) {
const auto reactions = &owner->reactions();
done(Ui::EmojiFlyAnimation(_controller->wrap(), reactions, {
.id = id,
.scaleOutDuration = kReactionScaleOutDuration,
.scaleOutTarget = kReactionScaleOutTarget,
}, [] {}, Data::CustomEmojiSizeTag::Isolated).grabBadgeCenter());
}
}
void Reactions::startReactionAnimation(
Ui::ReactionFlyAnimationArgs args,
not_null<QWidget*> target,
Fn<void(Ui::ReactionFlyCenter)> done) {
const auto wrap = _controller->wrap();
const auto story = _controller->story();
_reactionAnimation = std::make_unique<Ui::EmojiFlyAnimation>(
wrap,
&story->owner().reactions(),
std::move(args),
[=] { _reactionAnimation->repaint(); },
Data::CustomEmojiSizeTag::Isolated);
const auto layer = _reactionAnimation->layer();
wrap->paintRequest() | rpl::start_with_next([=] {
if (!_reactionAnimation->paintBadgeFrame(target)) {
InvokeQueued(layer, [=] {
_reactionAnimation = nullptr;
wrap->update();
});
if (done) {
done(_reactionAnimation->grabBadgeCenter());
}
}
}, layer->lifetime());
wrap->update();
}
} // namespace Media::Stories

View File

@ -7,10 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "data/data_message_reaction_id.h"
#include "ui/effects/animations.h"
namespace Data {
class DocumentMedia;
struct ReactionId;
class Session;
class Story;
} // namespace Data
namespace HistoryView::Reactions {
@ -20,47 +24,96 @@ struct ChosenReaction;
namespace Ui {
class RpWidget;
struct ReactionFlyAnimationArgs;
struct ReactionFlyCenter;
class EmojiFlyAnimation;
} // namespace Ui
namespace Media::Stories {
class Controller;
enum class ReactionsMode {
Message,
Reaction,
};
class Reactions final {
public:
explicit Reactions(not_null<Controller*> controller);
~Reactions();
using Chosen = HistoryView::Reactions::ChosenReaction;
[[nodiscard]] rpl::producer<bool> expandedValue() const {
return _expanded.value();
}
[[nodiscard]] rpl::producer<Chosen> chosen() const {
return _chosen.events();
}
using Mode = ReactionsMode;
template <typename Reaction>
struct ChosenWrap {
Reaction reaction;
Mode mode;
};
using Chosen = ChosenWrap<HistoryView::Reactions::ChosenReaction>;
[[nodiscard]] rpl::producer<bool> activeValue() const;
[[nodiscard]] rpl::producer<Chosen> chosen() const;
[[nodiscard]] Data::ReactionId liked() const;
[[nodiscard]] rpl::producer<Data::ReactionId> likedValue() const;
void showLikeFrom(Data::Story *story);
void show();
void hide();
void hideIfCollapsed();
void collapse();
void outsidePressed();
void toggleLiked();
void ready();
void setReplyFieldState(
rpl::producer<bool> focused,
rpl::producer<bool> hasSendText);
void attachToReactionButton(not_null<Ui::RpWidget*> button);
private:
struct Hiding;
class Panel;
void create();
void updateShowState();
void fadeOutSelector();
void animateAndProcess(Chosen &&chosen);
void assignLikedId(Data::ReactionId id);
[[nodiscard]] Fn<void(Ui::ReactionFlyCenter)> setLikedIdIconInit(
not_null<Data::Session*> owner,
Data::ReactionId id,
bool force = false);
void setLikedIdFrom(Data::Story *story);
void setLikedId(
not_null<Data::Session*> owner,
Data::ReactionId id,
bool force = false);
void startReactionAnimation(
Ui::ReactionFlyAnimationArgs from,
not_null<QWidget*> target,
Fn<void(Ui::ReactionFlyCenter)> done = nullptr);
void waitForLikeIcon(
not_null<Data::Session*> owner,
Data::ReactionId id);
void initLikeIcon(
not_null<Data::Session*> owner,
Data::ReactionId id,
Ui::ReactionFlyCenter center);
const not_null<Controller*> _controller;
const std::unique_ptr<Panel> _panel;
std::unique_ptr<Ui::RpWidget> _parent;
std::unique_ptr<HistoryView::Reactions::Selector> _selector;
std::vector<std::unique_ptr<Hiding>> _hiding;
rpl::event_stream<Chosen> _chosen;
Ui::Animations::Simple _showing;
rpl::variable<float64> _shownValue;
rpl::variable<bool> _expanded;
bool _shown = false;
bool _replyFocused = false;
bool _hasSendText = false;
Ui::RpWidget *_likeButton = nullptr;
rpl::variable<Data::ReactionId> _liked;
base::has_weak_ptr _likeIconGuard;
std::unique_ptr<Ui::RpWidget> _likeIcon;
std::shared_ptr<Data::DocumentMedia> _likeIconMedia;
std::unique_ptr<Ui::EmojiFlyAnimation> _reactionAnimation;
rpl::lifetime _likeIconWaitLifetime;
rpl::lifetime _likeFromLifetime;
rpl::lifetime _lifetime;
};

View File

@ -623,7 +623,7 @@ void ReplyArea::initActions() {
_controls->likeToggled(
) | rpl::start_with_next([=] {
_controller->toggleLiked(!_controller->liked());
_controller->toggleLiked();
}, _lifetime);
_controls->setMimeDataHook([=](
@ -649,7 +649,9 @@ void ReplyArea::initActions() {
_controls->showFinished();
}
void ReplyArea::show(ReplyAreaData data) {
void ReplyArea::show(
ReplyAreaData data,
rpl::producer<Data::ReactionId> likedValue) {
if (_data == data) {
return;
}
@ -666,7 +668,11 @@ void ReplyArea::show(ReplyAreaData data) {
const auto history = user ? user->owner().history(user).get() : nullptr;
_controls->setHistory({
.history = history,
.liked = _controller->likedValue(),
.liked = std::move(
likedValue
) | rpl::map([](const Data::ReactionId &id) {
return !id.empty();
}),
});
_controls->clear();
const auto hidden = user && user->isSelf();
@ -697,6 +703,10 @@ Main::Session &ReplyArea::session() const {
return _data.user->session();
}
bool ReplyArea::focused() const {
return _controls->focused();
}
rpl::producer<bool> ReplyArea::focusedValue() const {
return _controls->focusedValue();
}
@ -725,7 +735,7 @@ void ReplyArea::tryProcessKeyInput(not_null<QKeyEvent*> e) {
_controls->tryProcessKeyInput(e);
}
not_null<QWidget*> ReplyArea::likeAnimationTarget() const {
not_null<Ui::RpWidget*> ReplyArea::likeAnimationTarget() const {
return _controls->likeAnimationTarget();
}

View File

@ -41,6 +41,7 @@ class Session;
namespace Ui {
struct PreparedList;
class SendFilesWay;
class RpWidget;
} // namespace Ui
namespace Media::Stories {
@ -60,9 +61,12 @@ public:
explicit ReplyArea(not_null<Controller*> controller);
~ReplyArea();
void show(ReplyAreaData data);
void show(
ReplyAreaData data,
rpl::producer<Data::ReactionId> likedValue);
void sendReaction(const Data::ReactionId &id);
[[nodiscard]] bool focused() const;
[[nodiscard]] rpl::producer<bool> focusedValue() const;
[[nodiscard]] rpl::producer<bool> activeValue() const;
[[nodiscard]] rpl::producer<bool> hasSendTextValue() const;
@ -70,7 +74,7 @@ public:
[[nodiscard]] bool ignoreWindowMove(QPoint position) const;
void tryProcessKeyInput(not_null<QKeyEvent*> e);
[[nodiscard]] not_null<QWidget*> likeAnimationTarget() const;
[[nodiscard]] not_null<Ui::RpWidget*> likeAnimationTarget() const;
private:
class Cant;

View File

@ -682,7 +682,7 @@ storiesComposeControls: ComposeControls(defaultComposeControls) {
attach: storiesAttach;
emoji: storiesAttachEmoji;
like: storiesLike;
liked: icon{{ "chat/input_liked", settingsIconBg1 }};
liked: icon{};
suggestions: EmojiSuggestions(defaultEmojiSuggestions) {
dropdown: InnerDropdown(emojiSuggestionsDropdown) {
animation: PanelAnimation(defaultPanelAnimation) {
@ -807,6 +807,7 @@ storiesReactionsPan: EmojiPan(storiesEmojiPan) {
storiesReactionsWidth: 210px;
storiesReactionsBottomSkip: 29px;
storiesReactionsAddedTop: 200px;
storiesLikeReactionsPosition: point(85px, 30px);
storiesUnsupportedLabel: FlatLabel(defaultFlatLabel) {
textFg: mediaviewControlFg;

View File

@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/emoji_fly_animation.h"
#include "data/stickers/data_custom_emoji.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/animated_icon.h"
#include "styles/style_info.h"
#include "styles/style_chat.h"
@ -100,4 +102,10 @@ bool EmojiFlyAnimation::paintBadgeFrame(not_null<QWidget*> widget) {
return !_fly.finished();
}
ReactionFlyCenter EmojiFlyAnimation::grabBadgeCenter() {
auto result = _fly.takeCenter();
result.size = _flySize;
return result;
}
} // namespace Ui

View File

@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
struct ReactionFlyCenter;
class EmojiFlyAnimation {
public:
EmojiFlyAnimation(
@ -26,6 +28,7 @@ public:
void repaint();
bool paintBadgeFrame(not_null<QWidget*> widget);
[[nodiscard]] ReactionFlyCenter grabBadgeCenter();
private:
const int _flySize = 0;

View File

@ -68,7 +68,8 @@ ReactionFlyAnimation::ReactionFlyAnimation(
: _owner(owner)
, _repaint(std::move(repaint))
, _flyFrom(args.flyFrom)
, _scaleOutDuration(args.scaleOutDuration) {
, _scaleOutDuration(args.scaleOutDuration)
, _scaleOutTarget(args.scaleOutTarget) {
const auto &list = owner->list(::Data::Reactions::Type::All);
auto centerIcon = (DocumentData*)nullptr;
auto aroundAnimation = (DocumentData*)nullptr;
@ -86,12 +87,14 @@ ReactionFlyAnimation::ReactionFlyAnimation(
aroundAnimation = owner->chooseGenericAnimation(document);
} else {
const auto i = ranges::find(list, args.id, &::Data::Reaction::id);
if (i == end(list) || !i->centerIcon) {
if (i == end(list)/* || !i->centerIcon*/) {
return;
}
centerIcon = i->centerIcon;
centerIcon = i->centerIcon
? not_null(i->centerIcon)
: i->selectAnimation;
aroundAnimation = i->aroundAnimation;
_centerSizeMultiplier = 1.;
_centerSizeMultiplier = i->centerIcon ? 1. : 0.5;
}
const auto resolve = [&](
std::unique_ptr<AnimatedIcon> &icon,
@ -139,21 +142,31 @@ QRect ReactionFlyAnimation::paintGetArea(
QRect clip,
crl::time now) const {
const auto scale = [&] {
const auto rate = _effect ? _effect->frameRate() : 0.;
if (!_scaleOutDuration || !rate) {
if (!_scaleOutDuration
|| (!_effect && !_noEffectScaleStarted)) {
return 1.;
}
const auto left = _effect->framesCount() - _effect->frameIndex();
const auto duration = left * 1000. / rate;
return (duration < _scaleOutDuration)
? (duration / double(_scaleOutDuration))
: 1.;
auto progress = _noEffectScaleAnimation.value(0.);
if (_effect) {
const auto rate = _effect->frameRate();
if (!rate) {
return 1.;
}
const auto left = _effect->framesCount() - _effect->frameIndex();
const auto duration = left * 1000. / rate;
progress = (duration < _scaleOutDuration)
? (duration / double(_scaleOutDuration))
: 1.;
}
return (1. * progress + _scaleOutTarget * (1. - progress));
}();
auto hq = std::optional<PainterHighQualityEnabler>();
if (scale < 1.) {
const auto delta = ((1. - scale) / 2.) * target.size();
target = QRect(
target.topLeft() + QPoint(delta.width(), delta.height()),
target.size() * scale);
hq.emplace(p);
const auto shift = QRectF(target).center();
p.translate(shift);
p.scale(scale, scale);
p.translate(-shift);
}
if (!_valid) {
return QRect();
@ -169,8 +182,10 @@ QRect ReactionFlyAnimation::paintGetArea(
if (clip.isEmpty() || area.intersects(clip)) {
paintCenterFrame(p, target, colored, now);
if (const auto effect = _effect.get()) {
// Must not be colored to text.
p.drawImage(wide, effect->frame(QColor()));
if (effect->animating()) {
// Must not be colored to text.
p.drawImage(wide, effect->frame(QColor()));
}
}
paintMiniCopies(p, target.center(), colored, now);
}
@ -359,6 +374,9 @@ void ReactionFlyAnimation::startAnimations() {
}
if (const auto effect = _effect.get()) {
_effect->animate(callback());
} else if (_scaleOutDuration > 0) {
_noEffectScaleStarted = true;
_noEffectScaleAnimation.start(callback(), 1, 0, _scaleOutDuration);
}
if (!_miniCopies.empty()) {
_minis.start(callback(), 0., 1., kMiniCopiesDurationMax);
@ -382,7 +400,19 @@ bool ReactionFlyAnimation::finished() const {
|| (_flyIcon.isNull()
&& (!_center || !_center->animating())
&& (!_effect || !_effect->animating())
&& !_noEffectScaleAnimation.animating()
&& !_minis.animating());
}
ReactionFlyCenter ReactionFlyAnimation::takeCenter() {
_valid = false;
return {
.custom = std::move(_custom),
.icon = std::move(_center),
.scale = (_scaleOutDuration > 0) ? _scaleOutTarget : 1.,
.centerSizeMultiplier = _centerSizeMultiplier,
.customSize = _customSize,
};
}
} // namespace HistoryView::Reactions

View File

@ -28,11 +28,21 @@ struct ReactionFlyAnimationArgs {
QImage flyIcon;
QRect flyFrom;
crl::time scaleOutDuration = 0;
float64 scaleOutTarget = 0.;
bool effectOnly = false;
[[nodiscard]] ReactionFlyAnimationArgs translated(QPoint point) const;
};
struct ReactionFlyCenter {
std::unique_ptr<Text::CustomEmoji> custom;
std::unique_ptr<AnimatedIcon> icon;
float64 scale = 0.;
float64 centerSizeMultiplier = 0.;
int customSize = 0;
int size = 0;
};
class ReactionFlyAnimation final {
public:
ReactionFlyAnimation(
@ -56,6 +66,8 @@ public:
[[nodiscard]] float64 flyingProgress() const;
[[nodiscard]] bool finished() const;
[[nodiscard]] ReactionFlyCenter takeCenter();
private:
struct Parabolic {
float64 a = 0.;
@ -98,6 +110,7 @@ private:
std::unique_ptr<Text::CustomEmoji> _custom;
std::unique_ptr<AnimatedIcon> _center;
std::unique_ptr<AnimatedIcon> _effect;
Animations::Simple _noEffectScaleAnimation;
std::vector<MiniCopy> _miniCopies;
Animations::Simple _fly;
Animations::Simple _minis;
@ -105,6 +118,8 @@ private:
float64 _centerSizeMultiplier = 0.;
int _customSize = 0;
crl::time _scaleOutDuration = 0;
float64 _scaleOutTarget = 0.;
bool _noEffectScaleStarted = false;
bool _valid = false;
mutable Parabolic _cached;