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), NewAdded = (1U << 2),
ViewsAdded = (1U << 3), ViewsAdded = (1U << 3),
MarkRead = (1U << 4), MarkRead = (1U << 4),
Reaction = (1U << 5),
LastUsedBit = (1U << 4), LastUsedBit = (1U << 5),
}; };
using Flags = base::flags<Flag>; using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; } 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); loadImage(set, document, !i->centerIcon);
} else if (!_waitingForList) { } else if (!_waitingForList) {
_waitingForList = true; _waitingForList = true;
refreshRecent(); refreshDefault();
} }
} }

View file

@ -870,6 +870,21 @@ void Stories::activateStealthMode(Fn<void()> done) {
}).send(); }).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) { std::shared_ptr<HistoryItem> Stories::resolveItem(not_null<Story*> story) {
auto &items = _items[story->peer()->id]; auto &items = _items[story->peer()->id];
auto i = items.find(story->id()); auto i = items.find(story->id());

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,17 +26,16 @@ struct FileChosen;
namespace Data { namespace Data {
struct FileOrigin; struct FileOrigin;
struct ReactionId; class DocumentMedia;
} // namespace Data } // namespace Data
namespace HistoryView::Reactions { namespace HistoryView::Reactions {
class CachedIconFactory; class CachedIconFactory;
struct ChosenReaction;
} // namespace HistoryView::Reactions } // namespace HistoryView::Reactions
namespace Ui { namespace Ui {
class RpWidget; class RpWidget;
struct ReactionFlyAnimationArgs;
class EmojiFlyAnimation;
class BoxContent; class BoxContent;
} // namespace Ui } // namespace Ui
@ -66,6 +65,7 @@ struct SiblingView;
enum class SiblingType; enum class SiblingType;
struct ContentLayout; struct ContentLayout;
class CaptionFullView; class CaptionFullView;
enum class ReactionsMode;
enum class HeaderLayout { enum class HeaderLayout {
Normal, Normal,
@ -118,9 +118,7 @@ public:
[[nodiscard]] Data::FileOrigin fileOrigin() const; [[nodiscard]] Data::FileOrigin fileOrigin() const;
[[nodiscard]] TextWithEntities captionText() const; [[nodiscard]] TextWithEntities captionText() const;
[[nodiscard]] bool skipCaption() const; [[nodiscard]] bool skipCaption() const;
[[nodiscard]] bool liked() const; void toggleLiked();
[[nodiscard]] rpl::producer<bool> likedValue() const;
void toggleLiked(bool liked);
void showFullCaption(); void showFullCaption();
void captionClosing(); void captionClosing();
void captionClosed(); void captionClosed();
@ -172,6 +170,9 @@ public:
[[nodiscard]] rpl::lifetime &lifetime(); [[nodiscard]] rpl::lifetime &lifetime();
private: private:
class PhotoPlayback;
class Unsupported;
using ChosenReaction = HistoryView::Reactions::ChosenReaction;
struct StoriesList { struct StoriesList {
not_null<UserData*> user; not_null<UserData*> user;
Data::StoriesIds ids; Data::StoriesIds ids;
@ -194,8 +195,6 @@ private:
float64 rotation = 0.; float64 rotation = 0.;
ClickHandlerPtr handler; ClickHandlerPtr handler;
}; };
class PhotoPlayback;
class Unsupported;
void initLayout(); void initLayout();
bool changeShown(Data::Story *story); bool changeShown(Data::Story *story);
@ -238,9 +237,7 @@ private:
const std::vector<Data::StoriesSourceInfo> &lists, const std::vector<Data::StoriesSourceInfo> &lists,
int index); int index);
void startReactionAnimation( void reactionChosen(ReactionsMode mode, ChosenReaction chosen);
Ui::ReactionFlyAnimationArgs from,
not_null<QWidget*> target);
const not_null<Delegate*> _delegate; const not_null<Delegate*> _delegate;
@ -260,9 +257,7 @@ private:
bool _contentFaded = false; bool _contentFaded = false;
bool _windowActive = false; bool _windowActive = false;
bool _replyFocused = false;
bool _replyActive = false; bool _replyActive = false;
bool _hasSendText = false;
bool _layerShown = false; bool _layerShown = false;
bool _menuShown = false; bool _menuShown = false;
bool _tooltipShown = false; bool _tooltipShown = false;
@ -273,7 +268,6 @@ private:
Data::StoriesContext _context; Data::StoriesContext _context;
std::optional<Data::StoriesSource> _source; std::optional<Data::StoriesSource> _source;
std::optional<StoriesList> _list; std::optional<StoriesList> _list;
rpl::variable<bool> _liked;
FullStoryId _waitingForId; FullStoryId _waitingForId;
int _waitingForDelta = 0; int _waitingForDelta = 0;
int _index = 0; int _index = 0;
@ -297,7 +291,6 @@ private:
std::unique_ptr<Sibling> _siblingRight; std::unique_ptr<Sibling> _siblingRight;
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker; std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
std::unique_ptr<Ui::EmojiFlyAnimation> _reactionAnimation;
Main::Session *_session = nullptr; Main::Session *_session = nullptr;
rpl::lifetime _sessionLifetime; 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 "media/stories/media_stories_reactions.h"
#include "base/event_filter.h"
#include "boxes/premium_preview_box.h" #include "boxes/premium_preview_box.h"
#include "chat_helpers/compose/compose_show.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_message_reactions.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "history/view/reactions/history_view_reactions_selector.h" #include "history/view/reactions/history_view_reactions_selector.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "media/stories/media_stories_controller.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_chat_helpers.h"
#include "styles/style_media_view.h" #include "styles/style_media_view.h"
#include "styles/style_widgets.h" #include "styles/style_widgets.h"
@ -21,6 +29,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Media::Stories { namespace Media::Stories {
namespace { 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( [[nodiscard]] Data::PossibleItemReactionsRef LookupPossibleReactions(
not_null<Main::Session*> session) { not_null<Main::Session*> session) {
auto result = Data::PossibleItemReactionsRef(); auto result = Data::PossibleItemReactionsRef();
@ -51,7 +67,50 @@ namespace {
} // 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) { explicit Hiding(not_null<QWidget*> parent) : widget(parent) {
} }
@ -60,16 +119,24 @@ struct Reactions::Hiding {
QImage frame; QImage frame;
}; };
Reactions::Reactions(not_null<Controller*> controller) Reactions::Panel::Panel(not_null<Controller*> controller)
: _controller(controller) { : _controller(controller) {
} }
Reactions::~Reactions() = default; Reactions::Panel::~Panel() = default;
void Reactions::show() { auto Reactions::Panel::chosen() const -> rpl::producer<Chosen> {
if (_shown) { return _chosen.events();
}
void Reactions::Panel::show(Mode mode) {
const auto was = _mode.current();
if (_shown.current() && was == mode) {
return; return;
} else if (_shown.current()) {
hide(was);
} }
_mode = mode;
create(); create();
if (!_selector) { if (!_selector) {
return; return;
@ -82,8 +149,8 @@ void Reactions::show() {
_parent->show(); _parent->show();
} }
void Reactions::hide() { void Reactions::Panel::hide(Mode mode) {
if (!_selector) { if (!_selector || _mode.current() != mode) {
return; return;
} }
_selector->beforeDestroy(); _selector->beforeDestroy();
@ -97,20 +164,32 @@ void Reactions::hide() {
_parent = nullptr; _parent = nullptr;
} }
void Reactions::hideIfCollapsed() { void Reactions::Panel::hideIfCollapsed(Mode mode) {
if (!_expanded.current()) { if (!_expanded.current() && _mode.current() == mode) {
hide(); hide(mode);
} }
} }
void Reactions::collapse() { void Reactions::Panel::collapse(Mode mode) {
if (_expanded.current()) { if (_expanded.current() && _mode.current() == mode) {
hide(); hide(mode);
show(); 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( auto reactions = LookupPossibleReactions(
&_controller->uiShow()->session()); &_controller->uiShow()->session());
if (reactions.recent.empty() && !reactions.morePremiumAvailable) { if (reactions.recent.empty() && !reactions.morePremiumAvailable) {
@ -119,13 +198,19 @@ void Reactions::create() {
_parent = std::make_unique<Ui::RpWidget>(_controller->wrap().get()); _parent = std::make_unique<Ui::RpWidget>(_controller->wrap().get());
_parent->show(); _parent->show();
const auto mode = _mode.current();
_parent->events() | rpl::start_with_next([=](not_null<QEvent*> e) { _parent->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) { if (e->type() == QEvent::MouseButtonPress) {
const auto event = static_cast<QMouseEvent*>(e.get()); const auto event = static_cast<QMouseEvent*>(e.get());
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton) {
if (!_selector if (!_selector
|| !_selector->geometry().contains(event->pos())) { || !_selector->geometry().contains(event->pos())) {
collapse(); if (mode == Mode::Message) {
collapse(mode);
} else {
hide(mode);
}
} }
} }
} }
@ -137,17 +222,17 @@ void Reactions::create() {
_controller->uiShow(), _controller->uiShow(),
std::move(reactions), std::move(reactions),
_controller->cachedReactionIconFactory().createMethod(), _controller->cachedReactionIconFactory().createMethod(),
[=](bool fast) { hide(); }); [=](bool fast) { hide(mode); });
_selector->chosen( _selector->chosen(
) | rpl::start_with_next([=]( ) | rpl::start_with_next([=](
HistoryView::Reactions::ChosenReaction reaction) { HistoryView::Reactions::ChosenReaction reaction) {
_chosen.fire_copy(reaction); _chosen.fire({ .reaction = reaction, .mode = mode });
hide(); hide(mode);
}, _selector->lifetime()); }, _selector->lifetime());
_selector->premiumPromoChosen() | rpl::start_with_next([=] { _selector->premiumPromoChosen() | rpl::start_with_next([=] {
hide(); hide(mode);
ShowPremiumPreviewBox( ShowPremiumPreviewBox(
_controller->uiShow(), _controller->uiShow(),
PremiumPreview::InfiniteReactions); PremiumPreview::InfiniteReactions);
@ -165,13 +250,23 @@ void Reactions::create() {
_controller->layoutValue(), _controller->layoutValue(),
_shownValue.value() _shownValue.value()
) | rpl::start_with_next([=](const Layout &layout, float64 shown) { ) | rpl::start_with_next([=](const Layout &layout, float64 shown) {
const auto shift = int(base::SafeRound((full / 2.) * shown)); const auto width = extents.left()
_parent->setGeometry(QRect( + _selector->countAppearedWidth(shown)
layout.reactions.x() + layout.reactions.width() / 2 - shift, + extents.right();
layout.reactions.y(), const auto height = layout.reactions.height();
full, const auto shift = (width / 2);
layout.reactions.height())); const auto right = (mode == Mode::Message)
const auto innerTop = layout.reactions.height() ? (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::storiesReactionsBottomSkip
- st::reactStripHeight; - st::reactStripHeight;
const auto maxAdded = innerTop - extents.top() - categoriesTop; const auto maxAdded = innerTop - extents.top() - categoriesTop;
@ -186,11 +281,15 @@ void Reactions::create() {
}, _selector->lifetime()); }, _selector->lifetime());
_selector->escapes() | rpl::start_with_next([=] { _selector->escapes() | rpl::start_with_next([=] {
collapse(); if (mode == Mode::Message) {
collapse(mode);
} else {
hide(mode);
}
}, _selector->lifetime()); }, _selector->lifetime());
} }
void Reactions::fadeOutSelector() { void Reactions::Panel::fadeOutSelector() {
const auto wrap = _controller->wrap().get(); const auto wrap = _controller->wrap().get();
const auto geometry = Ui::MapFrom( const auto geometry = Ui::MapFrom(
wrap, wrap,
@ -226,8 +325,8 @@ void Reactions::fadeOutSelector() {
}); });
} }
void Reactions::updateShowState() { void Reactions::Panel::updateShowState() {
const auto progress = _showing.value(_shown ? 1. : 0.); const auto progress = _showing.value(_shown.current() ? 1. : 0.);
const auto opacity = 1.; const auto opacity = 1.;
const auto appearing = _showing.animating(); const auto appearing = _showing.animating();
const auto toggling = false; const auto toggling = false;
@ -235,4 +334,355 @@ void Reactions::updateShowState() {
_selector->updateShowState(progress, opacity, appearing, toggling); _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 } // namespace Media::Stories

View file

@ -7,10 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "data/data_message_reaction_id.h"
#include "ui/effects/animations.h" #include "ui/effects/animations.h"
namespace Data { namespace Data {
class DocumentMedia;
struct ReactionId; struct ReactionId;
class Session;
class Story;
} // namespace Data } // namespace Data
namespace HistoryView::Reactions { namespace HistoryView::Reactions {
@ -20,47 +24,96 @@ struct ChosenReaction;
namespace Ui { namespace Ui {
class RpWidget; class RpWidget;
struct ReactionFlyAnimationArgs;
struct ReactionFlyCenter;
class EmojiFlyAnimation;
} // namespace Ui } // namespace Ui
namespace Media::Stories { namespace Media::Stories {
class Controller; class Controller;
enum class ReactionsMode {
Message,
Reaction,
};
class Reactions final { class Reactions final {
public: public:
explicit Reactions(not_null<Controller*> controller); explicit Reactions(not_null<Controller*> controller);
~Reactions(); ~Reactions();
using Chosen = HistoryView::Reactions::ChosenReaction; using Mode = ReactionsMode;
[[nodiscard]] rpl::producer<bool> expandedValue() const {
return _expanded.value(); template <typename Reaction>
} struct ChosenWrap {
[[nodiscard]] rpl::producer<Chosen> chosen() const { Reaction reaction;
return _chosen.events(); 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 hide();
void hideIfCollapsed(); void outsidePressed();
void collapse(); void toggleLiked();
void ready();
void setReplyFieldState(
rpl::producer<bool> focused,
rpl::producer<bool> hasSendText);
void attachToReactionButton(not_null<Ui::RpWidget*> button);
private: private:
struct Hiding; class Panel;
void create(); void animateAndProcess(Chosen &&chosen);
void updateShowState();
void fadeOutSelector(); 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 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; rpl::event_stream<Chosen> _chosen;
Ui::Animations::Simple _showing; bool _replyFocused = false;
rpl::variable<float64> _shownValue; bool _hasSendText = false;
rpl::variable<bool> _expanded;
bool _shown = 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( _controls->likeToggled(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
_controller->toggleLiked(!_controller->liked()); _controller->toggleLiked();
}, _lifetime); }, _lifetime);
_controls->setMimeDataHook([=]( _controls->setMimeDataHook([=](
@ -649,7 +649,9 @@ void ReplyArea::initActions() {
_controls->showFinished(); _controls->showFinished();
} }
void ReplyArea::show(ReplyAreaData data) { void ReplyArea::show(
ReplyAreaData data,
rpl::producer<Data::ReactionId> likedValue) {
if (_data == data) { if (_data == data) {
return; return;
} }
@ -666,7 +668,11 @@ void ReplyArea::show(ReplyAreaData data) {
const auto history = user ? user->owner().history(user).get() : nullptr; const auto history = user ? user->owner().history(user).get() : nullptr;
_controls->setHistory({ _controls->setHistory({
.history = history, .history = history,
.liked = _controller->likedValue(), .liked = std::move(
likedValue
) | rpl::map([](const Data::ReactionId &id) {
return !id.empty();
}),
}); });
_controls->clear(); _controls->clear();
const auto hidden = user && user->isSelf(); const auto hidden = user && user->isSelf();
@ -697,6 +703,10 @@ Main::Session &ReplyArea::session() const {
return _data.user->session(); return _data.user->session();
} }
bool ReplyArea::focused() const {
return _controls->focused();
}
rpl::producer<bool> ReplyArea::focusedValue() const { rpl::producer<bool> ReplyArea::focusedValue() const {
return _controls->focusedValue(); return _controls->focusedValue();
} }
@ -725,7 +735,7 @@ void ReplyArea::tryProcessKeyInput(not_null<QKeyEvent*> e) {
_controls->tryProcessKeyInput(e); _controls->tryProcessKeyInput(e);
} }
not_null<QWidget*> ReplyArea::likeAnimationTarget() const { not_null<Ui::RpWidget*> ReplyArea::likeAnimationTarget() const {
return _controls->likeAnimationTarget(); return _controls->likeAnimationTarget();
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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