Initial reaction effects implementation.

This commit is contained in:
John Preston 2022-01-11 17:13:04 +03:00
parent db453ab7ae
commit 0ab26f0c82
33 changed files with 733 additions and 107 deletions

View File

@ -638,6 +638,8 @@ PRIVATE
history/view/history_view_pinned_section.h
history/view/history_view_pinned_tracker.cpp
history/view/history_view_pinned_tracker.h
history/view/history_view_react_animation.cpp
history/view/history_view_react_animation.h
history/view/history_view_react_button.cpp
history/view/history_view_react_button.h
history/view/history_view_reactions.cpp

View File

@ -224,19 +224,21 @@ void Reactions::request() {
MTP_int(_hash)
)).done([=](const MTPmessages_AvailableReactions &result) {
_requestId = 0;
const auto oldCache = base::take(_iconsCache);
const auto toCache = [&](DocumentData *document) {
if (document) {
_iconsCache.emplace(document, document->createMediaView());
}
};
result.match([&](const MTPDmessages_availableReactions &data) {
_hash = data.vhash().v;
const auto &list = data.vreactions().v;
const auto oldCache = base::take(_iconsCache);
_active.clear();
_available.clear();
_active.reserve(list.size());
_available.reserve(list.size());
_iconsCache.reserve(list.size() * 2);
const auto toCache = [&](not_null<DocumentData*> document) {
_iconsCache.emplace(document, document->createMediaView());
};
for (const auto &reaction : list) {
if (const auto parsed = parse(reaction)) {
_available.push_back(*parsed);
@ -244,6 +246,8 @@ void Reactions::request() {
_active.push_back(*parsed);
toCache(parsed->appearAnimation);
toCache(parsed->selectAnimation);
toCache(parsed->centerIcon);
toCache(parsed->aroundAnimation);
}
}
}
@ -277,10 +281,10 @@ std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
.appearAnimation = _owner->processDocument(
data.vappear_animation()),
.selectAnimation = selectAnimation,
.activateAnimation = _owner->processDocument(
data.vactivate_animation()),
.activateEffects = _owner->processDocument(
data.veffect_animation()),
//.activateAnimation = _owner->processDocument(
// data.vactivate_animation()),
//.activateEffects = _owner->processDocument(
// data.veffect_animation()),
.centerIcon = (data.vcenter_icon()
? _owner->processDocument(*data.vcenter_icon()).get()
: nullptr),

View File

@ -24,8 +24,8 @@ struct Reaction {
not_null<DocumentData*> staticIcon;
not_null<DocumentData*> appearAnimation;
not_null<DocumentData*> selectAnimation;
not_null<DocumentData*> activateAnimation;
not_null<DocumentData*> activateEffects;
//not_null<DocumentData*> activateAnimation;
//not_null<DocumentData*> activateEffects;
DocumentData *centerIcon = nullptr;
DocumentData *aroundAnimation = nullptr;
bool active = false;

View File

@ -377,8 +377,21 @@ HistoryInner::HistoryInner(
using ChosenReaction = HistoryView::Reactions::Manager::Chosen;
_reactionsManager->chosen(
) | rpl::start_with_next([=](ChosenReaction reaction) {
if (const auto item = session().data().message(reaction.context)) {
item->toggleReaction(reaction.emoji);
const auto item = session().data().message(reaction.context);
if (!item) {
return;
}
item->toggleReaction(reaction.emoji);
if (item->chosenReaction() != reaction.emoji) {
return;
} else if (const auto view = item->mainView()) {
if (const auto top = itemTop(view); top >= 0) {
view->animateSendReaction({
.emoji = reaction.emoji,
.flyIcon = reaction.icon,
.flyFrom = reaction.geometry.translated(0, -top),
});
}
}
}, lifetime());

View File

@ -17,6 +17,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_react_animation.h"
#include "lottie/lottie_icon.h"
#include "data/data_message_reactions.h"
#include "styles/style_chat.h"
#include "styles/style_dialogs.h"
@ -31,6 +33,8 @@ BottomInfo::BottomInfo(
layout();
}
BottomInfo::~BottomInfo() = default;
void BottomInfo::update(Data &&data, int availableWidth) {
_data = std::move(data);
layout();
@ -221,19 +225,27 @@ void BottomInfo::paint(
left += width() - available;
top += st::msgDateFont->height;
}
paintReactions(p, left, top, available);
paintReactions(p, position, left, top, available);
}
}
void BottomInfo::paintReactions(
Painter &p,
QPoint origin,
int left,
int top,
int availableWidth) const {
auto x = left;
auto y = top;
auto widthLeft = availableWidth;
const auto animated = _reactionAnimation
? _reactionAnimation->playingAroundEmoji()
: QString();
if (_reactionAnimation && animated.isEmpty()) {
_reactionAnimation = nullptr;
}
for (const auto &reaction : _reactions) {
const auto animating = (reaction.emoji == animated);
const auto add = (reaction.countTextWidth > 0)
? st::reactionInfoDigitSkip
: st::reactionInfoBetween;
@ -251,11 +263,18 @@ void BottomInfo::paintReactions(
reaction.emoji,
::Data::Reactions::ImageSize::BottomInfo);
}
if (!reaction.image.isNull()) {
p.drawImage(
x + (st::reactionInfoSize - st::reactionInfoImage) / 2,
y + (st::msgDateFont->height - st::reactionInfoImage) / 2,
reaction.image);
const auto image = QRect(
x + (st::reactionInfoSize - st::reactionInfoImage) / 2,
y + (st::msgDateFont->height - st::reactionInfoImage) / 2,
st::reactionInfoImage,
st::reactionInfoImage);
const auto skipImage = animating
&& (reaction.count < 2 || !_reactionAnimation->flying());
if (!reaction.image.isNull() && !skipImage) {
p.drawImage(image.topLeft(), reaction.image);
}
if (animating) {
_reactionAnimation->paint(p, origin, image);
}
if (reaction.countTextWidth > 0) {
p.drawText(
@ -406,6 +425,26 @@ void BottomInfo::setReactionCount(Reaction &reaction, int count) {
: 0;
}
void BottomInfo::animateReactionSend(
SendReactionAnimationArgs &&args,
Fn<void()> repaint) {
_reactionAnimation = std::make_unique<Reactions::SendAnimation>(
_reactionsOwner,
args.translated(QPoint(width(), height())),
std::move(repaint),
st::reactionInfoImage);
}
auto BottomInfo::takeSendReactionAnimation()
-> std::unique_ptr<Reactions::SendAnimation> {
return std::move(_reactionAnimation);
}
void BottomInfo::continueSendReactionAnimation(
std::unique_ptr<Reactions::SendAnimation> animation) {
_reactionAnimation = std::move(animation);
}
BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
using Flag = BottomInfo::Data::Flag;
const auto item = message->message();

View File

@ -20,11 +20,15 @@ class Reactions;
} // namespace Data
namespace HistoryView {
namespace Reactions {
class SendAnimation;
} // namespace Reactions
using PaintContext = Ui::ChatPaintContext;
class Message;
struct TextState;
struct SendReactionAnimationArgs;
class BottomInfo final : public Object {
public:
@ -49,6 +53,7 @@ public:
Flags flags;
};
BottomInfo(not_null<::Data::Reactions*> reactionsOwner, Data &&data);
~BottomInfo();
void update(Data &&data, int availableWidth);
@ -67,6 +72,14 @@ public:
bool inverted,
const PaintContext &context) const;
void animateReactionSend(
SendReactionAnimationArgs &&args,
Fn<void()> repaint);
[[nodiscard]] auto takeSendReactionAnimation()
-> std::unique_ptr<Reactions::SendAnimation>;
void continueSendReactionAnimation(
std::unique_ptr<Reactions::SendAnimation> animation);
private:
struct Reaction {
mutable QImage image;
@ -86,6 +99,7 @@ private:
[[nodiscard]] int countReactionsHeight(int newWidth) const;
void paintReactions(
Painter &p,
QPoint origin,
int left,
int top,
int availableWidth) const;
@ -102,6 +116,7 @@ private:
Ui::Text::String _views;
Ui::Text::String _replies;
std::vector<Reaction> _reactions;
mutable std::unique_ptr<Reactions::SendAnimation> _reactionAnimation;
int _reactionsMaxWidth = 0;
int _dateWidth = 0;
bool _authorElided = false;

View File

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_media_grouped.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_large_emoji.h"
#include "history/view/history_view_react_animation.h"
#include "history/view/history_view_react_button.h"
#include "history/view/history_view_cursor_state.h"
#include "history/history.h"
@ -341,6 +342,15 @@ void DateBadge::paint(
ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide);
}
SendReactionAnimationArgs SendReactionAnimationArgs::translated(
QPoint point) const {
return {
.emoji = emoji,
.flyIcon = flyIcon,
.flyFrom = flyFrom.translated(point),
};
}
Element::Element(
not_null<ElementDelegate*> delegate,
not_null<HistoryItem*> data,
@ -392,6 +402,10 @@ void Element::setY(int y) {
void Element::refreshDataIdHook() {
}
void Element::repaint() const {
history()->owner().requestViewRepaint(this);
}
void Element::paintHighlight(
Painter &p,
const PaintContext &context,
@ -1020,7 +1034,7 @@ void Element::clickHandlerActiveChanged(
}
}
App::hoveredLinkItem(active ? this : nullptr);
history()->owner().requestViewRepaint(this);
repaint();
if (const auto media = this->media()) {
media->clickHandlerActiveChanged(handler, active);
}
@ -1035,12 +1049,20 @@ void Element::clickHandlerPressedChanged(
}
}
App::pressedLinkItem(pressed ? this : nullptr);
history()->owner().requestViewRepaint(this);
repaint();
if (const auto media = this->media()) {
media->clickHandlerPressedChanged(handler, pressed);
}
}
void Element::animateSendReaction(SendReactionAnimationArgs &&args) {
}
auto Element::takeSendReactionAnimation()
-> std::unique_ptr<Reactions::SendAnimation> {
return nullptr;
}
Element::~Element() {
// Delete media while owner still exists.
base::take(_media);

View File

@ -33,6 +33,10 @@ struct ChatPaintContext;
class ChatStyle;
} // namespace Ui
namespace Lottie {
class Icon;
} // namespace Lottie
namespace HistoryView {
enum class PointState : char;
@ -45,6 +49,7 @@ using PaintContext = Ui::ChatPaintContext;
namespace Reactions {
struct ButtonParameters;
class SendAnimation;
} // namespace Reactions
enum class Context : char {
@ -224,6 +229,14 @@ struct DateBadge : public RuntimeComponent<DateBadge, Element> {
};
struct SendReactionAnimationArgs {
QString emoji;
std::shared_ptr<Lottie::Icon> flyIcon;
QRect flyFrom;
[[nodiscard]] SendReactionAnimationArgs translated(QPoint point) const;
};
class Element
: public Object
, public RuntimeComposer<Element>
@ -407,9 +420,15 @@ public:
[[nodiscard]] bool markSponsoredViewed(int shownFromTop) const;
virtual void animateSendReaction(SendReactionAnimationArgs &&args);
[[nodiscard]] virtual auto takeSendReactionAnimation()
-> std::unique_ptr<Reactions::SendAnimation>;
virtual ~Element();
protected:
void repaint() const;
void paintHighlight(
Painter &p,
const PaintContext &context,

View File

@ -342,8 +342,21 @@ ListWidget::ListWidget(
using ChosenReaction = Reactions::Manager::Chosen;
_reactionsManager->chosen(
) | rpl::start_with_next([=](ChosenReaction reaction) {
if (const auto item = session().data().message(reaction.context)) {
item->toggleReaction(reaction.emoji);
const auto item = session().data().message(reaction.context);
if (!item) {
return;
}
item->toggleReaction(reaction.emoji);
if (item->chosenReaction() != reaction.emoji) {
return;
} else if (const auto view = viewForItem(item)) {
if (const auto top = itemTop(view); top >= 0) {
view->animateSendReaction({
.emoji = reaction.emoji,
.flyIcon = reaction.icon,
.flyFrom = reaction.geometry.translated(0, -top),
});
}
}
}, lifetime());

View File

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_message.h"
#include "history/view/media/history_view_media.h"
#include "history/view/media/history_view_web_page.h"
#include "history/view/history_view_react_animation.h"
#include "history/view/history_view_react_button.h"
#include "history/view/history_view_reactions.h"
#include "history/view/history_view_group_call_bar.h" // UserpicInRow.
@ -252,6 +253,17 @@ Message::Message(
initLogEntryOriginal();
initPsa();
refreshReactions();
auto animation = replacing
? replacing->takeSendReactionAnimation()
: nullptr;
if (animation) {
animation->setRepaintCallback([=] { repaint(); });
if (_reactions) {
_reactions->continueSendAnimation(std::move(animation));
} else {
_bottomInfo.continueSendReactionAnimation(std::move(animation));
}
}
}
Message::~Message() {
@ -314,6 +326,111 @@ void Message::applyGroupAdminChanges(
}
}
void Message::animateSendReaction(SendReactionAnimationArgs &&args) {
const auto item = message();
const auto media = this->media();
auto g = countGeometry();
if (g.width() < 1 || isHidden()) {
return;
}
const auto repainter = [=] { repaint(); };
const auto bubble = drawBubble();
const auto reactionsInBubble = _reactions && embedReactionsInBubble();
const auto mediaDisplayed = media && media->isDisplayed();
auto keyboard = item->inlineReplyKeyboard();
auto keyboardHeight = 0;
if (keyboard) {
keyboardHeight = keyboard->naturalHeight();
g.setHeight(g.height() - st::msgBotKbButton.margin - keyboardHeight);
}
if (_reactions && !reactionsInBubble) {
const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();
const auto reactionsLeft = (!bubble && mediaDisplayed)
? media->contentRectForReactions().x()
: 0;
g.setHeight(g.height() - reactionsHeight);
const auto reactionsPosition = QPoint(reactionsLeft + g.left(), g.top() + g.height() + st::mediaInBubbleSkip);
_reactions->animateSend(args.translated(-reactionsPosition), repainter);
return;
}
const auto animateInBottomInfo = [&](QPoint bottomRight) {
_bottomInfo.animateReactionSend(args.translated(-bottomRight), repainter);
};
if (bubble) {
auto entry = logEntryOriginal();
// Entry page is always a bubble bottom.
auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/);
auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());
auto inner = g;
if (_comments) {
inner.setHeight(inner.height() - st::historyCommentsButtonHeight);
}
auto trect = inner.marginsRemoved(st::msgPadding);
const auto reactionsTop = (reactionsInBubble && !_viewButton)
? st::mediaInBubbleSkip
: 0;
const auto reactionsHeight = reactionsInBubble
? (reactionsTop + _reactions->height())
: 0;
if (reactionsInBubble) {
trect.setHeight(trect.height() - reactionsHeight);
const auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + reactionsTop);
_reactions->animateSend(args.translated(-reactionsPosition), repainter);
return;
}
if (_viewButton) {
const auto belowInfo = _viewButton->belowMessageInfo();
const auto infoHeight = reactionsInBubble
? (reactionsHeight + st::msgPadding.bottom())
: _bottomInfo.height();
const auto heightMargins = QMargins(0, 0, 0, infoHeight);
if (belowInfo) {
inner -= heightMargins;
}
trect.setHeight(trect.height() - _viewButton->height());
if (reactionsInBubble) {
trect.setHeight(trect.height() + st::msgPadding.bottom());
} else if (mediaDisplayed) {
trect.setHeight(trect.height() - st::mediaInBubbleSkip);
}
}
if (mediaOnBottom) {
trect.setHeight(trect.height()
+ st::msgPadding.bottom()
- viewButtonHeight());
}
if (mediaOnTop) {
trect.setY(trect.y() - st::msgPadding.top());
}
if (mediaDisplayed && mediaOnBottom && media->customInfoLayout()) {
auto mediaHeight = media->height();
auto mediaLeft = trect.x() - st::msgPadding.left();
auto mediaTop = (trect.y() + trect.height() - mediaHeight);
animateInBottomInfo(QPoint(mediaLeft, mediaTop) + media->resolveCustomInfoRightBottom());
} else {
animateInBottomInfo({
inner.left() + inner.width() - (st::msgPadding.right() - st::msgDateDelta.x()),
inner.top() + inner.height() - (st::msgPadding.bottom() - st::msgDateDelta.y()),
});
}
} else if (mediaDisplayed) {
animateInBottomInfo(g.topLeft() + media->resolveCustomInfoRightBottom());
}
}
auto Message::takeSendReactionAnimation()
-> std::unique_ptr<Reactions::SendAnimation> {
return _reactions
? _reactions->takeSendAnimation()
: _bottomInfo.takeSendReactionAnimation();
}
QSize Message::performCountOptimalSize() {
const auto item = message();
const auto media = this->media();
@ -516,12 +633,12 @@ void Message::draw(Painter &p, const PaintContext &context) const {
const auto stm = context.messageStyle();
const auto bubble = drawBubble();
auto dateh = 0;
if (const auto date = Get<DateBadge>()) {
dateh = date->height();
}
if (const auto bar = Get<UnreadBar>()) {
auto unreadbarh = bar->height();
auto dateh = 0;
if (const auto date = Get<DateBadge>()) {
dateh = date->height();
}
if (context.clip.intersects(QRect(0, dateh, width(), unreadbarh))) {
p.translate(0, dateh);
bar->paint(
@ -1207,7 +1324,7 @@ void Message::toggleCommentsButtonRipple(bool pressed) {
_comments->ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
[=] { history()->owner().requestViewRepaint(this); });
[=] { repaint(); });
}
_comments->ripple->add(_comments->lastPoint);
} else if (_comments->ripple) {
@ -1653,7 +1770,7 @@ void Message::psaTooltipToggled(bool tooltipShown) const {
state->buttonVisible = visible;
history()->owner().notifyViewLayoutChange(this);
state->buttonVisibleAnimation.start(
[=] { history()->owner().requestViewRepaint(this); },
[=] { repaint(); },
visible ? 0. : 1.,
visible ? 1. : 0.,
st::fadeWrapDuration);
@ -2009,14 +2126,17 @@ void Message::refreshReactions() {
auto reactionsData = InlineListDataFromMessage(this);
if (!_reactions) {
const auto handlerFactory = [=](QString emoji) {
const auto weak = base::make_weak(this);
const auto fullId = data()->fullId();
return std::make_shared<LambdaClickHandler>([=](
ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
const auto &data = controller->session().data();
if (const auto item = data.message(fullId)) {
item->toggleReaction(emoji);
return std::make_shared<LambdaClickHandler>([=] {
if (const auto strong = weak.get()) {
strong->data()->toggleReaction(emoji);
if (const auto now = weak.get()) {
if (now->data()->chosenReaction() == emoji) {
now->animateSendReaction({
.emoji = emoji,
});
}
}
}
});
@ -2044,7 +2164,7 @@ void Message::itemDataChanged() {
if (wasInfo != nowInfo || wasReactions != nowReactions) {
history()->owner().requestViewResize(this);
} else {
history()->owner().requestViewRepaint(this);
repaint();
}
}
@ -2097,10 +2217,10 @@ void Message::updateViewButtonExistence() {
} else if (_viewButton) {
return;
}
auto callback = [=] { history()->owner().requestViewRepaint(this); };
auto repainter = [=] { repaint(); };
_viewButton = sponsored
? std::make_unique<ViewButton>(sponsored, std::move(callback))
: std::make_unique<ViewButton>(media, std::move(callback));
? std::make_unique<ViewButton>(sponsored, std::move(repainter))
: std::make_unique<ViewButton>(media, std::move(repainter));
}
void Message::initLogEntryOriginal() {

View File

@ -134,6 +134,10 @@ public:
void applyGroupAdminChanges(
const base::flat_set<UserId> &changes) override;
void animateSendReaction(SendReactionAnimationArgs &&args) override;
auto takeSendReactionAnimation()
-> std::unique_ptr<Reactions::SendAnimation> override;
protected:
void refreshDataIdHook() override;

View File

@ -0,0 +1,135 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/history_view_react_animation.h"
#include "history/view/history_view_element.h"
#include "lottie/lottie_icon.h"
#include "data/data_message_reactions.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
namespace HistoryView::Reactions {
namespace {
constexpr auto kFlyDuration = crl::time(200);
} // namespace
SendAnimation::SendAnimation(
not_null<::Data::Reactions*> owner,
SendReactionAnimationArgs &&args,
Fn<void()> repaint,
int size)
: _owner(owner)
, _emoji(args.emoji)
, _repaint(std::move(repaint))
, _flyFrom(args.flyFrom) {
const auto &list = owner->list(::Data::Reactions::Type::All);
const auto i = ranges::find(list, _emoji, &::Data::Reaction::emoji);
if (i == end(list) || !i->centerIcon) {
return;
}
const auto resolve = [&](
std::unique_ptr<Lottie::Icon> &icon,
DocumentData *document,
int size) {
if (!document) {
return false;
}
const auto media = document->activeMediaView();
if (!media || !media->loaded()) {
return false;
}
icon = std::make_unique<Lottie::Icon>(Lottie::IconDescriptor{
.path = document->filepath(true),
.json = media->bytes(),
.sizeOverride = QSize(size, size),
});
return true;
};
_flyIcon = std::move(args.flyIcon);
if (!resolve(_center, i->centerIcon, size)
|| !resolve(_effect, i->aroundAnimation, size * 2)) {
return;
}
if (_flyIcon) {
_fly.start([=] { flyCallback(); }, 0., 1., kFlyDuration);
} else {
startAnimations();
}
_valid = true;
}
SendAnimation::~SendAnimation() = default;
void SendAnimation::paint(QPainter &p, QPoint origin, QRect target) const {
if (_flyIcon) {
const auto from = _flyFrom.translated(origin);
const auto lshift = target.width() / 4;
const auto rshift = target.width() / 2 - lshift;
const auto margins = QMargins{ lshift, lshift, rshift, rshift };
target = target.marginsRemoved(margins);
const auto progress = _fly.value(1.);
const auto rect = QRect(
anim::interpolate(from.x(), target.x(), progress),
anim::interpolate(from.y(), target.y(), progress),
anim::interpolate(from.width(), target.width(), progress),
anim::interpolate(from.height(), target.height(), progress));
auto hq = PainterHighQualityEnabler(p);
if (progress < 1.) {
p.setOpacity(1. - progress);
p.drawImage(rect, _flyIcon->frame());
}
if (progress > 0.) {
p.setOpacity(progress);
p.drawImage(rect.marginsAdded(margins), _center->frame());
}
p.setOpacity(1.);
} else {
p.drawImage(target, _center->frame());
p.drawImage(QRect(
target.topLeft() - QPoint(target.width(), target.height()) / 2,
target.size() * 2
), _effect->frame());
}
}
void SendAnimation::startAnimations() {
_center->animate([=] { callback(); }, 0, _center->framesCount() - 1);
_effect->animate([=] { callback(); }, 0, _effect->framesCount() - 1);
}
void SendAnimation::flyCallback() {
if (!_fly.animating()) {
_flyIcon = nullptr;
startAnimations();
}
callback();
}
void SendAnimation::callback() {
if (_repaint) {
_repaint();
}
}
void SendAnimation::setRepaintCallback(Fn<void()> repaint) {
_repaint = std::move(repaint);
}
bool SendAnimation::flying() const {
return (_flyIcon != nullptr);
}
QString SendAnimation::playingAroundEmoji() const {
const auto finished = !_valid
|| (!_flyIcon && !_center->animating() && !_effect->animating());
return finished ? QString() : _emoji;
}
} // namespace HistoryView::Reactions

View File

@ -0,0 +1,58 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/effects/animations.h"
namespace Lottie {
class Icon;
} // namespace Lottie
namespace Data {
class Reactions;
} // namespace Data
namespace HistoryView {
struct SendReactionAnimationArgs;
} // namespace HistoryView
namespace HistoryView::Reactions {
class SendAnimation final {
public:
SendAnimation(
not_null<::Data::Reactions*> owner,
SendReactionAnimationArgs &&args,
Fn<void()> repaint,
int size);
~SendAnimation();
void setRepaintCallback(Fn<void()> repaint);
void paint(QPainter &p, QPoint origin, QRect target) const;
[[nodiscard]] QString playingAroundEmoji() const;
[[nodiscard]] bool flying() const;
private:
void flyCallback();
void startAnimations();
void callback();
const not_null<::Data::Reactions*> _owner;
const QString _emoji;
Fn<void()> _repaint;
std::shared_ptr<Lottie::Icon> _flyIcon;
std::unique_ptr<Lottie::Icon> _center;
std::unique_ptr<Lottie::Icon> _effect;
Ui::Animations::Simple _fly;
QRect _flyFrom;
bool _valid = false;
};
} // namespace HistoryView::Reactions

View File

@ -70,6 +70,20 @@ constexpr auto kHoverScale = 1.24;
return style::ConvertScale(kSizeForDownscale);
}
[[nodiscard]] std::shared_ptr<Lottie::Icon> CreateIcon(
not_null<Data::DocumentMedia*> media,
int size,
int frame) {
Expects(media->loaded());
return std::make_shared<Lottie::Icon>(Lottie::IconDescriptor{
.path = media->owner()->filepath(true),
.json = media->bytes(),
.sizeOverride = QSize(size, size),
.frame = frame,
});
}
} // namespace
Button::Button(
@ -383,17 +397,58 @@ Manager::Manager(
_createChooseCallback = [=](QString emoji) {
return [=] {
if (const auto context = _buttonContext) {
if (auto chosen = lookupChosen(emoji)) {
updateButton({});
_chosen.fire({
.context = context,
.emoji = emoji,
});
_chosen.fire(std::move(chosen));
}
};
};
}
Manager::Chosen Manager::lookupChosen(const QString &emoji) const {
auto result = Chosen{
.context = _buttonContext,
.emoji = emoji,
};
const auto button = _button.get();
const auto i = ranges::find(_icons, emoji, &ReactionIcons::emoji);
if (i == end(_icons) || !button) {
return result;
}
const auto &icon = *i;
if (const auto &appear = icon->appear; appear && appear->animating()) {
result.icon = CreateIcon(
icon->appearAnimation->activeMediaView().get(),
appear->width(),
appear->frameIndex());
} else if (const auto &select = icon->select) {
result.icon = CreateIcon(
icon->selectAnimation->activeMediaView().get(),
select->width(),
select->frameIndex());
}
const auto index = (i - begin(_icons));
const auto between = st::reactionCornerSkip;
const auto oneHeight = (st::reactionCornerSize.height() + between);
const auto expanded = (_icons.size() > 1);
const auto skip = (expanded ? st::reactionExpandedSkip : 0);
const auto scroll = button->scroll();
const auto local = skip + index * oneHeight - scroll;
const auto geometry = button->geometry();
const auto top = button->expandUp()
? (geometry.height() - local - _outer.height())
: local;
const auto rect = QRect(geometry.topLeft() + QPoint(0, top), _outer);
const auto imageSize = int(base::SafeRound(
st::reactionCornerImage * kHoverScale));
result.geometry = QRect(
rect.x() + (rect.width() - imageSize) / 2,
rect.y() + (rect.height() - imageSize) / 2,
imageSize,
imageSize);
return result;
}
void Manager::applyListFilters() {
const auto limit = _uniqueLimit.current();
const auto applyUniqueLimit = _buttonContext
@ -480,15 +535,13 @@ void Manager::showButtonDelayed() {
}
void Manager::applyList(const std::vector<Data::Reaction> &list) {
constexpr auto predicate = [](
const Data::Reaction &a,
const Data::Reaction &b) {
return (a.emoji == b.emoji)
&& (a.appearAnimation == b.appearAnimation)
&& (a.selectAnimation == b.selectAnimation);
};
const auto proj = [](const auto &obj) {
return std::tie(obj.emoji, obj.appearAnimation, obj.selectAnimation);
return std::tie(
obj.emoji,
obj.appearAnimation,
obj.selectAnimation,
obj.centerIcon,
obj.aroundAnimation);
};
if (ranges::equal(_list, list, ranges::equal_to(), proj, proj)) {
return;
@ -502,6 +555,8 @@ void Manager::applyList(const std::vector<Data::Reaction> &list) {
.emoji = reaction.emoji,
.appearAnimation = reaction.appearAnimation,
.selectAnimation = reaction.selectAnimation,
.centerIcon = reaction.centerIcon,
.aroundAnimation = reaction.aroundAnimation,
});
}
applyListFilters();
@ -649,6 +704,7 @@ void Manager::loadIcons() {
}
return entry.icon;
};
auto all = true;
for (const auto &icon : _icons) {
if (!icon->appear) {
icon->appear = load(icon->appearAnimation);
@ -656,6 +712,23 @@ void Manager::loadIcons() {
if (!icon->select) {
icon->select = load(icon->selectAnimation);
}
if (!icon->appear || !icon->select) {
all = false;
}
}
if (all) {
const auto preload = [&](DocumentData *document) {
const auto view = document
? document->activeMediaView()
: nullptr;
if (view) {
view->checkStickerLarge();
}
};
for (const auto &icon : _icons) {
preload(icon->centerIcon);
preload(icon->aroundAnimation);
}
}
}
@ -821,6 +894,7 @@ void Manager::paintButton(
if (opacity == 0.) {
return;
}
const auto geometry = button->geometry();
const auto position = geometry.topLeft();
const auto size = geometry.size();
@ -1469,13 +1543,7 @@ IconFactory CachedIconFactory::createMethod() {
std::shared_ptr<Lottie::Icon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,
int size) {
Expects(media->loaded());
return std::make_shared<Lottie::Icon>(Lottie::IconDescriptor{
.path = media->owner()->filepath(true),
.json = media->bytes(),
.sizeOverride = QSize(size, size),
});
return CreateIcon(media, size, 0);
}
} // namespace HistoryView

View File

@ -154,6 +154,12 @@ public:
struct Chosen {
FullMsgId context;
QString emoji;
std::shared_ptr<Lottie::Icon> icon;
QRect geometry;
explicit operator bool() const {
return context && !emoji.isNull();
}
};
[[nodiscard]] rpl::producer<Chosen> chosen() const {
return _chosen.events();
@ -172,6 +178,8 @@ private:
QString emoji;
not_null<DocumentData*> appearAnimation;
not_null<DocumentData*> selectAnimation;
DocumentData *centerIcon = nullptr;
DocumentData *aroundAnimation = nullptr;
std::shared_ptr<Lottie::Icon> appear;
std::shared_ptr<Lottie::Icon> select;
mutable ClickHandlerPtr link;
@ -190,6 +198,7 @@ private:
void showButtonDelayed();
void stealWheelEvents(not_null<QWidget*> target);
[[nodiscard]] Chosen lookupChosen(const QString &emoji) const;
[[nodiscard]] bool overCurrentButton(QPoint position) const;
void removeStaleButtons();

View File

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_react_animation.h"
#include "data/data_message_reactions.h"
#include "lang/lang_tag.h"
#include "ui/chat/chat_style.h"
@ -39,6 +40,8 @@ InlineList::InlineList(
layout();
}
InlineList::~InlineList() = default;
void InlineList::update(Data &&data, int availableWidth) {
_data = std::move(data);
layout();
@ -187,11 +190,19 @@ void InlineList::paint(
const auto size = st::reactionBottomSize;
const auto skip = (size - st::reactionBottomImage) / 2;
const auto inbubble = (_data.flags & InlineListData::Flag::InBubble);
const auto animated = _animation
? _animation->playingAroundEmoji()
: QString();
if (_animation && animated.isEmpty()) {
_animation = nullptr;
}
p.setFont(st::semiboldFont);
for (const auto &button : _buttons) {
const auto animating = (animated == button.emoji);
const auto &geometry = button.geometry;
const auto inner = geometry.marginsRemoved(padding);
const auto chosen = (_data.chosenReaction == button.emoji);
const auto chosen = (_data.chosenReaction == button.emoji)
&& (!animating || !_animation->flying());
{
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
@ -216,8 +227,16 @@ void InlineList::paint(
button.emoji,
::Data::Reactions::ImageSize::InlineList);
}
if (!button.image.isNull()) {
p.drawImage(inner.topLeft() + QPoint(skip, skip), button.image);
const auto image = QRect(
inner.topLeft() + QPoint(skip, skip),
QSize(st::reactionBottomImage, st::reactionBottomImage));
const auto skipImage = animating
&& (button.count < 2 || !_animation->flying());
if (!button.image.isNull() && !skipImage) {
p.drawImage(image.topLeft(), button.image);
}
if (animating) {
_animation->paint(p, QPoint(), image);
}
p.setPen(!inbubble
? (chosen
@ -262,6 +281,25 @@ bool InlineList::getState(
return false;
}
void InlineList::animateSend(
SendReactionAnimationArgs &&args,
Fn<void()> repaint) {
_animation = std::make_unique<Reactions::SendAnimation>(
_owner,
std::move(args),
std::move(repaint),
st::reactionBottomImage);
}
std::unique_ptr<SendAnimation> InlineList::takeSendAnimation() {
return std::move(_animation);
}
void InlineList::continueSendAnimation(
std::unique_ptr<SendAnimation> animation) {
_animation = std::move(animation);
}
InlineListData InlineListDataFromMessage(not_null<Message*> message) {
using Flag = InlineListData::Flag;
const auto item = message->message();

View File

@ -21,10 +21,13 @@ namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
class Message;
struct TextState;
struct SendReactionAnimationArgs;
} // namespace HistoryView
namespace HistoryView::Reactions {
class SendAnimation;
struct InlineListData {
enum class Flag : uchar {
InBubble = 0x01,
@ -45,6 +48,7 @@ public:
not_null<::Data::Reactions*> owner,
Fn<ClickHandlerPtr(QString)> handlerFactory,
Data &&data);
~InlineList();
void update(Data &&data, int availableWidth);
QSize countCurrentSize(int newWidth) override;
@ -63,6 +67,12 @@ public:
QPoint point,
not_null<TextState*> outResult) const;
void animateSend(
SendReactionAnimationArgs &&args,
Fn<void()> repaint);
[[nodiscard]] std::unique_ptr<SendAnimation> takeSendAnimation();
void continueSendAnimation(std::unique_ptr<SendAnimation> animation);
private:
struct Button {
QRect geometry;
@ -88,6 +98,8 @@ private:
std::vector<Button> _buttons;
QSize _skipBlock;
mutable std::unique_ptr<SendAnimation> _animation;
};
[[nodiscard]] InlineListData InlineListDataFromMessage(

View File

@ -798,7 +798,7 @@ void Document::updatePressed(QPoint point) {
/ float64(width() - nameleft - nameright),
0.,
1.));
history()->owner().requestViewRepaint(_parent);
repaint();
}
}
}
@ -1008,7 +1008,7 @@ bool Document::voiceProgressAnimationCallback(crl::time now) {
} else {
voice->_playback->progress.update(qMin(dt, 1.), anim::linear);
}
history()->owner().requestViewRepaint(_parent);
repaint();
return (dt < 1.);
}
}

View File

@ -30,21 +30,17 @@ void File::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
if (p == _savel || p == _cancell) {
if (active && !dataLoaded()) {
ensureAnimation();
_animation->a_thumbOver.start([this] { thumbAnimationCallback(); }, 0., 1., st::msgFileOverDuration);
_animation->a_thumbOver.start([=] { repaint(); }, 0., 1., st::msgFileOverDuration);
} else if (!active && _animation && !dataLoaded()) {
_animation->a_thumbOver.start([this] { thumbAnimationCallback(); }, 1., 0., st::msgFileOverDuration);
_animation->a_thumbOver.start([=] { repaint(); }, 1., 0., st::msgFileOverDuration);
}
}
}
void File::thumbAnimationCallback() {
history()->owner().requestViewRepaint(_parent);
}
void File::clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) {
history()->owner().requestViewRepaint(_parent);
repaint();
}
void File::setLinks(
@ -92,7 +88,7 @@ void File::radialAnimationCallback(crl::time now) const {
now);
}();
if (!anim::Disabled() || updated) {
history()->owner().requestViewRepaint(_parent);
repaint();
}
if (!_animation->radial.animating()) {
checkAnimationFinished();

View File

@ -66,7 +66,6 @@ protected:
void setStatusSize(int newSize, int fullSize, int duration, qint64 realDuration) const;
void radialAnimationCallback(crl::time now) const;
void thumbAnimationCallback();
void ensureAnimation() const;
void checkAnimationFinished() const;

View File

@ -1203,28 +1203,44 @@ std::optional<int> Gif::reactionButtonCenterOverride() const {
if (!isSeparateRoundVideo()) {
return std::nullopt;
}
const auto right = resolveCustomInfoRightBottom().x()
- _parent->infoWidth()
- 3 * st::msgDateImgPadding.x();
return right - st::reactionCornerSize.width() / 2;
}
QPoint Gif::resolveCustomInfoRightBottom() const {
const auto inner = contentRectForReactions();
auto fullBottom = inner.y() + inner.height();
auto fullRight = inner.x() + inner.width();
auto maxRight = _parent->width() - st::msgMargin.left();
if (_parent->hasFromPhoto()) {
maxRight -= st::msgMargin.right();
} else {
maxRight -= st::msgMargin.left();
}
const auto infoWidth = _parent->infoWidth();
const auto outbg = _parent->hasOutLayout();
const auto rightAligned = outbg
&& !_parent->delegate()->elementIsChatWide();
if (!rightAligned) {
// This is just some arbitrary point,
// the main idea is to make info left aligned here.
fullRight += infoWidth - st::normalFont->height;
if (fullRight > maxRight) {
fullRight = maxRight;
const auto isRound = isSeparateRoundVideo();
if (isRound) {
auto maxRight = _parent->width() - st::msgMargin.left();
if (_parent->hasFromPhoto()) {
maxRight -= st::msgMargin.right();
} else {
maxRight -= st::msgMargin.left();
}
const auto infoWidth = _parent->infoWidth();
const auto outbg = _parent->hasOutLayout();
const auto rightAligned = outbg
&& !_parent->delegate()->elementIsChatWide();
if (!rightAligned) {
// This is just some arbitrary point,
// the main idea is to make info left aligned here.
fullRight += infoWidth - st::normalFont->height;
if (fullRight > maxRight) {
fullRight = maxRight;
}
}
}
const auto right = fullRight - infoWidth - 3 * st::msgDateImgPadding.x();
return right - st::reactionCornerSize.width() / 2;
const auto skipx = isRound
? st::msgDateImgPadding.x()
: (st::msgDateImgDelta + st::msgDateImgPadding.x());
const auto skipy = isRound
? st::msgDateImgPadding.y()
: (st::msgDateImgDelta + st::msgDateImgPadding.y());
return QPoint(fullRight - skipx, fullBottom - skipy);
}
int Gif::additionalWidth() const {
@ -1544,7 +1560,7 @@ void Gif::repaintStreamedContent() {
&& !activeRoundStreamed()) {
return;
}
history()->owner().requestViewRepaint(_parent);
repaint();
}
void Gif::streamingReady(::Media::Streaming::Information &&info) {

View File

@ -98,6 +98,7 @@ public:
}
QRect contentRectForReactions() const override;
std::optional<int> reactionButtonCenterOverride() const override;
QPoint resolveCustomInfoRightBottom() const override;
QString additionalInfoString() const override;
bool skipBubbleTail() const override {

View File

@ -345,6 +345,12 @@ bool Location::needsBubble() const {
|| _parent->displayFromName();
}
QPoint Location::resolveCustomInfoRightBottom() const {
const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());
const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());
return QPoint(width() - skipx, height() - skipy);
}
int Location::fullWidth() const {
return st::locationSize.width();
}

View File

@ -53,6 +53,7 @@ public:
bool customInfoLayout() const override {
return true;
}
QPoint resolveCustomInfoRightBottom() const override;
bool skipBubbleTail() const override {
return isRoundedInBubbleBottom();

View File

@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lottie/lottie_single_player.h"
#include "storage/storage_shared_media.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "ui/item_text_options.h"
#include "ui/chat/chat_style.h"
@ -186,6 +187,10 @@ QSize Media::countCurrentSize(int newWidth) {
return QSize(qMin(newWidth, maxWidth()), minHeight());
}
void Media::repaint() const {
history()->owner().requestViewRepaint(_parent);
}
Ui::Text::String Media::createCaption(not_null<HistoryItem*> item) const {
if (item->emptyText()) {
return {};

View File

@ -212,6 +212,9 @@ public:
-> std::optional<int> {
return std::nullopt;
}
[[nodiscard]] virtual QPoint resolveCustomInfoRightBottom() const {
return QPoint();
}
[[nodiscard]] virtual QMargins bubbleMargins() const {
return QMargins();
}
@ -311,6 +314,8 @@ protected:
[[nodiscard]] bool usesBubblePattern(const PaintContext &context) const;
void repaint() const;
const not_null<Element*> _parent;
MediaInBubbleState _inBubbleState = MediaInBubbleState::None;

View File

@ -730,6 +730,12 @@ bool GroupedMedia::needsBubble() const {
return _needBubble;
}
QPoint GroupedMedia::resolveCustomInfoRightBottom() const {
const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());
const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());
return QPoint(width() - skipx, height() - skipy);
}
bool GroupedMedia::computeNeedBubble() const {
if (!_caption.isEmpty() || _mode == Mode::Column) {
return true;

View File

@ -84,6 +84,8 @@ public:
bool customInfoLayout() const override {
return _caption.isEmpty() && (_mode != Mode::Column);
}
QPoint resolveCustomInfoRightBottom() const override;
bool allowsFastShare() const override {
return true;
}

View File

@ -441,6 +441,15 @@ std::optional<int> UnwrappedMedia::reactionButtonCenterOverride() const {
return right - st::reactionCornerSize.width() / 2;
}
QPoint UnwrappedMedia::resolveCustomInfoRightBottom() const {
const auto inner = contentRectForReactions();
const auto fullBottom = inner.y() + inner.height();
const auto fullRight = calculateFullRight(inner);
const auto skipx = st::msgDateImgPadding.x();
const auto skipy = st::msgDateImgPadding.y();
return QPoint(fullRight - skipx, fullBottom - skipy);
}
std::unique_ptr<Lottie::SinglePlayer> UnwrappedMedia::stickerTakeLottie(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {

View File

@ -83,6 +83,8 @@ public:
}
QRect contentRectForReactions() const override;
std::optional<int> reactionButtonCenterOverride() const override;
QPoint resolveCustomInfoRightBottom() const override;
void stickerClearLoopPlayed() override {
_content->stickerClearLoopPlayed();
}

View File

@ -750,11 +750,11 @@ void Photo::repaintStreamedContent() {
} else if (_parent->delegate()->elementIsGifPaused()) {
return;
}
history()->owner().requestViewRepaint(_parent);
repaint();
}
void Photo::streamingReady(::Media::Streaming::Information &&info) {
history()->owner().requestViewRepaint(_parent);
repaint();
}
void Photo::checkAnimation() {
@ -831,6 +831,12 @@ bool Photo::needsBubble() const {
|| _parent->displayFromName());
}
QPoint Photo::resolveCustomInfoRightBottom() const {
const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());
const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());
return QPoint(width() - skipx, height() - skipy);
}
bool Photo::isReadyForOpen() const {
ensureDataMediaCreated();
return _dataMedia->loaded();

View File

@ -82,6 +82,7 @@ public:
bool customInfoLayout() const override {
return _caption.isEmpty();
}
QPoint resolveCustomInfoRightBottom() const override;
bool skipBubbleTail() const override {
return isRoundedInBubbleBottom() && _caption.isEmpty();
}

View File

@ -425,10 +425,10 @@ void Poll::checkQuizAnswered() {
}
if (i->correct) {
_fireworksAnimation = std::make_unique<Ui::FireworksAnimation>(
[=] { history()->owner().requestViewRepaint(_parent); });
[=] { repaint(); });
} else {
_wrongAnswerAnimation.start(
[=] { history()->owner().requestViewRepaint(_parent); },
[=] { repaint(); },
0.,
1.,
kRollDuration,
@ -455,7 +455,7 @@ void Poll::solutionToggled(
if (animated == anim::type::instant
&& _solutionButtonAnimation.animating()) {
_solutionButtonAnimation.stop();
history()->owner().requestViewRepaint(_parent);
repaint();
}
return;
}
@ -463,10 +463,10 @@ void Poll::solutionToggled(
history()->owner().notifyViewLayoutChange(_parent);
if (animated == anim::type::instant) {
_solutionButtonAnimation.stop();
history()->owner().requestViewRepaint(_parent);
repaint();
} else {
_solutionButtonAnimation.start(
[=] { history()->owner().requestViewRepaint(_parent); },
[=] { repaint(); },
visible ? 0. : 1.,
visible ? 1. : 0.,
st::fadeWrapDuration);
@ -562,7 +562,7 @@ void Poll::toggleMultiOption(const QByteArray &option) {
const auto selected = i->selected;
i->selected = !selected;
i->selectedAnimation.start(
[=] { history()->owner().requestViewRepaint(_parent); },
[=] { repaint(); },
selected ? 1. : 0.,
selected ? 0. : 1.,
st::defaultCheck.duration);
@ -575,7 +575,7 @@ void Poll::toggleMultiOption(const QByteArray &option) {
} else {
_hasSelected = true;
}
history()->owner().requestViewRepaint(_parent);
repaint();
}
}
@ -861,7 +861,7 @@ void Poll::resetAnswersAnimation() const {
void Poll::radialAnimationCallback() const {
if (!anim::Disabled()) {
history()->owner().requestViewRepaint(_parent);
repaint();
}
}
@ -930,7 +930,7 @@ void Poll::paintCloseByTimer(
_close = std::make_unique<CloseInformation>(
_poll->closeDate,
_poll->closePeriod,
[=] { history()->owner().requestViewRepaint(_parent); });
[=] { repaint(); });
}
const auto now = crl::now();
const auto left = std::max(_close->finish - now, crl::time(0));
@ -940,7 +940,7 @@ void Poll::paintCloseByTimer(
} else if (left < radial && !anim::Disabled()) {
if (!_close->radial.animating()) {
_close->radial.init([=] {
history()->owner().requestViewRepaint(_parent);
repaint();
});
_close->radial.start();
}
@ -1342,7 +1342,7 @@ void Poll::startAnswersAnimation() const {
data.correct = data.correct || answer.correct;
}
_answersAnimation->progress.start(
[=] { history()->owner().requestViewRepaint(_parent); },
[=] { repaint(); },
0.,
1.,
st::historyPollDuration);
@ -1524,7 +1524,7 @@ void Poll::toggleRipple(Answer &answer, bool pressed) {
answer.ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
[=] { history()->owner().requestViewRepaint(_parent); });
[=] { repaint(); });
}
const auto top = countAnswerTop(answer, innerWidth);
answer.ripple->add(_lastLinkPoint - QPoint(0, top));
@ -1587,7 +1587,7 @@ void Poll::toggleLinkRipple(bool pressed) {
_linkRipple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
[=] { history()->owner().requestViewRepaint(_parent); });
[=] { repaint(); });
}
_linkRipple->add(_lastLinkPoint - QPoint(0, height() - linkHeight));
} else if (_linkRipple) {