mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-16 06:07:06 +02:00
Implement custom reactions in stories.
This commit is contained in:
parent
066dbfe8fc
commit
13f67d68c4
21 changed files with 744 additions and 192 deletions
|
@ -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; }
|
||||
|
|
|
@ -381,7 +381,7 @@ void Reactions::preloadImageFor(const ReactionId &id) {
|
|||
loadImage(set, document, !i->centerIcon);
|
||||
} else if (!_waitingForList) {
|
||||
_waitingForList = true;
|
||||
refreshRecent();
|
||||
refreshDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue