init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,942 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/reactions/history_view_reactions.h"
#include "history/history_item.h"
#include "history/history.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_group_call_bar.h"
#include "core/click_handler_types.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_message_reactions.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "lang/lang_tag.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/reaction_fly_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/power_saving.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace HistoryView::Reactions {
namespace {
constexpr auto kInNonChosenOpacity = 0.12;
constexpr auto kOutNonChosenOpacity = 0.18;
constexpr auto kMaxRecentUserpics = 3;
constexpr auto kMaxNicePerRow = 5;
[[nodiscard]] QColor AdaptChosenServiceFg(QColor serviceBg) {
serviceBg.setAlpha(std::max(serviceBg.alpha(), 192));
return serviceBg;
}
} // namespace
struct InlineList::Button {
QRect geometry;
mutable std::unique_ptr<Ui::ReactionFlyAnimation> animation;
mutable QImage image;
mutable ClickHandlerPtr link;
mutable std::unique_ptr<Ui::Text::CustomEmoji> custom;
std::unique_ptr<Userpics> userpics;
ReactionId id;
QString text;
int textWidth = 0;
int count = 0;
bool chosen = false;
bool paid = false;
bool tag = false;
};
InlineList::InlineList(
not_null<::Data::Reactions*> owner,
Fn<ClickHandlerPtr(ReactionId)> handlerFactory,
Fn<void()> customEmojiRepaint,
Data &&data)
: _owner(owner)
, _handlerFactory(std::move(handlerFactory))
, _customEmojiRepaint(std::move(customEmojiRepaint))
, _data(std::move(data)) {
layout();
}
InlineList::~InlineList() = default;
void InlineList::update(Data &&data, int availableWidth) {
_data = std::move(data);
layout();
if (width() > 0) {
resizeGetHeight(std::min(maxWidth(), availableWidth));
}
}
void InlineList::updateSkipBlock(int width, int height) {
_skipBlock = { width, height };
}
void InlineList::removeSkipBlock() {
_skipBlock = {};
}
bool InlineList::areTags() const {
return _data.flags & Data::Flag::Tags;
}
std::vector<ReactionId> InlineList::computeTagsList() const {
if (!areTags()) {
return {};
}
return _buttons | ranges::views::transform(
&Button::id
) | ranges::to_vector;
}
bool InlineList::hasCustomEmoji() const {
return _hasCustomEmoji;
}
void InlineList::unloadCustomEmoji() {
if (!hasCustomEmoji()) {
return;
}
for (const auto &button : _buttons) {
if (const auto custom = button.custom.get()) {
custom->unload();
}
}
_customCache = QImage();
}
void InlineList::layout() {
layoutButtons();
initDimensions();
}
void InlineList::layoutButtons() {
if (_data.reactions.empty()) {
_buttons.clear();
return;
}
auto sorted = ranges::views::all(
_data.reactions
) | ranges::views::transform([](const MessageReaction &reaction) {
return not_null{ &reaction };
}) | ranges::to_vector;
const auto tags = areTags();
if (!tags) {
const auto &list = _owner->list(::Data::Reactions::Type::All);
ranges::sort(sorted, [&](
not_null<const MessageReaction*> a,
not_null<const MessageReaction*> b) {
const auto acount = a->count - (a->my ? 1 : 0);
const auto bcount = b->count - (b->my ? 1 : 0);
if (b->id.paid()) {
return false;
} else if (a->id.paid()) {
return true;
} else if (acount > bcount) {
return true;
} else if (acount < bcount) {
return false;
}
return ranges::find(list, a->id, &::Data::Reaction::id)
< ranges::find(list, b->id, &::Data::Reaction::id);
});
}
_hasCustomEmoji = false;
auto buttons = std::vector<Button>();
buttons.reserve(sorted.size());
for (const auto &reaction : sorted) {
const auto &id = reaction->id;
const auto i = ranges::find(_buttons, id, &Button::id);
buttons.push_back((i != end(_buttons))
? std::move(*i)
: prepareButtonWithId(id));
if (tags) {
setButtonTag(buttons.back(), _owner->myTagTitle(id));
} else if (const auto j = _data.recent.find(id)
; j != end(_data.recent) && !j->second.empty()) {
setButtonUserpics(buttons.back(), j->second);
} else {
setButtonCount(buttons.back(), reaction->count);
}
buttons.back().chosen = reaction->my;
if (id.custom()) {
_hasCustomEmoji = true;
}
}
_buttons = std::move(buttons);
}
InlineList::Dimension InlineList::countDimension(int width) const {
using Flag = InlineListData::Flag;
const auto inBubble = (_data.flags & Flag::InBubble);
const auto centered = (_data.flags & Flag::Centered);
const auto useWidth = centered
? std::min(width, st::chatGiveawayWidth)
: width;
const auto left = inBubble
? st::reactionInlineInBubbleLeft
: centered
? ((width - useWidth) / 2)
: 0;
return { .left = left, .width = useWidth };
}
InlineList::Button InlineList::prepareButtonWithId(const ReactionId &id) {
auto result = Button{ .id = id, .paid = id.paid()};
if (const auto customId = id.custom()) {
result.custom = _owner->owner().customEmojiManager().create(
customId,
_customEmojiRepaint);
} else {
_owner->preloadReactionImageFor(id);
}
return result;
}
void InlineList::setButtonTag(Button &button, const QString &title) {
if (button.tag && button.text == title) {
return;
}
button.userpics = nullptr;
button.count = 0;
button.tag = true;
button.text = title;
button.textWidth = st::reactionInlineTagFont->width(button.text);
}
void InlineList::setButtonCount(Button &button, int count) {
if (!button.tag && button.count == count && !button.userpics) {
return;
}
button.userpics = nullptr;
button.count = count;
button.tag = false;
if (count == 0) {
button.text = QString();
button.textWidth = 0;
} else {
button.text = Lang::FormatCountToShort(count).string;
button.textWidth = st::semiboldFont->width(button.text);
}
}
void InlineList::setButtonUserpics(
Button &button,
const std::vector<not_null<PeerData*>> &peers) {
button.tag = false;
if (!button.userpics) {
button.userpics = std::make_unique<Userpics>();
}
const auto count = button.count = int(peers.size());
auto &list = button.userpics->list;
const auto regenerate = [&] {
if (list.size() != count) {
return true;
}
for (auto i = 0; i != count; ++i) {
if (peers[i] != list[i].peer) {
return true;
}
}
return false;
}();
if (!regenerate) {
return;
}
auto generated = std::vector<UserpicInRow>();
generated.reserve(count);
for (auto i = 0; i != count; ++i) {
if (i == list.size()) {
list.push_back(UserpicInRow{
peers[i]
});
} else if (list[i].peer != peers[i]) {
list[i].peer = peers[i];
}
}
while (list.size() > count) {
list.pop_back();
}
button.userpics->image = QImage();
}
QSize InlineList::countOptimalSize() {
if (_buttons.empty()) {
return _skipBlock;
}
const auto left = countDimension(width()).left;
auto x = left;
const auto between = st::reactionInlineBetween;
const auto padding = st::reactionInlinePadding;
const auto size = st::reactionInlineSize;
const auto widthBaseTag = padding.left()
+ size
+ st::reactionInlineTagSkip
+ padding.right();
const auto widthBaseCount = padding.left()
+ size
+ st::reactionInlineSkip
+ padding.right();
const auto widthBaseUserpics = padding.left()
+ size
+ st::reactionInlineUserpicsPadding.left()
+ st::reactionInlineUserpicsPadding.right();
const auto userpicsWidth = [](const Button &button) {
const auto count = int(button.userpics->list.size());
const auto single = st::reactionInlineUserpics.size;
const auto shift = st::reactionInlineUserpics.shift;
const auto width = single + (count - 1) * (single - shift);
return width;
};
const auto height = padding.top() + size + padding.bottom();
for (auto &button : _buttons) {
const auto width = button.tag
? (widthBaseTag
+ button.textWidth
+ (button.textWidth ? st::reactionInlineSkip : 0))
: button.userpics
? (widthBaseUserpics + userpicsWidth(button))
: button.count == 0
? (rect::m::sum::h(padding) + size - st::reactionInlineEmptySkip)
: (widthBaseCount + button.textWidth);
button.geometry.setSize({ width, height });
x += width + between;
}
return QSize(
x - between + _skipBlock.width(),
std::max(height, _skipBlock.height()));
}
QSize InlineList::countCurrentSize(int newWidth) {
_data.flags &= ~Data::Flag::Flipped;
if (_buttons.empty()) {
return optimalSize();
}
using Flag = InlineListData::Flag;
const auto between = st::reactionInlineBetween;
const auto dimension = countDimension(newWidth);
const auto left = dimension.left;
const auto width = dimension.width;
const auto centered = (_data.flags & Flag::Centered);
auto x = left;
auto y = 0;
const auto recenter = [&](int beforeIndex) {
const auto added = centered ? (left + width + between - x) : 0;
if (added <= 0) {
return;
}
const auto shift = added / 2;
for (auto j = beforeIndex; j != 0;) {
auto &button = _buttons[--j];
if (button.geometry.y() != y) {
break;
}
button.geometry.translate(shift, 0);
}
};
for (auto i = 0, count = int(_buttons.size()); i != count; ++i) {
auto &button = _buttons[i];
const auto size = button.geometry.size();
if (x > left && x + size.width() > left + width) {
recenter(i);
x = left;
y += size.height() + between;
}
button.geometry = QRect(QPoint(x, y), size);
x += size.width() + between;
}
recenter(_buttons.size());
const auto &last = _buttons.back().geometry;
const auto height = y + last.height();
const auto right = last.x() + last.width() + _skipBlock.width();
const auto add = (right > width) ? _skipBlock.height() : 0;
return { newWidth, height + add };
}
int InlineList::countNiceWidth() const {
const auto count = _data.reactions.size();
const auto rows = (count + kMaxNicePerRow - 1) / kMaxNicePerRow;
const auto columns = (count + rows - 1) / rows;
const auto between = st::reactionInlineBetween;
auto result = 0;
auto inrow = 0;
auto x = 0;
for (auto &button : _buttons) {
if (inrow++ >= columns) {
x = 0;
inrow = 0;
}
x += button.geometry.width() + between;
accumulate_max(result, x - between);
}
return result;
}
void InlineList::flipToRight() {
_data.flags |= Data::Flag::Flipped;
for (auto &button : _buttons) {
button.geometry.moveLeft(
width() - button.geometry.x() - button.geometry.width());
}
}
int InlineList::placeAndResizeGetHeight(QRect available) {
const auto result = resizeGetHeight(available.width());
for (auto &button : _buttons) {
button.geometry.translate(available.x(), 0);
}
return result;
}
void InlineList::paint(
Painter &p,
const PaintContext &context,
int outerWidth,
const QRect &clip) const {
struct SingleAnimation {
not_null<Ui::ReactionFlyAnimation*> animation;
QColor textColor;
QRect target;
};
std::vector<SingleAnimation> animations;
auto finished = std::vector<std::unique_ptr<Ui::ReactionFlyAnimation>>();
const auto st = context.st;
const auto stm = context.messageStyle();
const auto padding = st::reactionInlinePadding;
const auto size = st::reactionInlineSize;
const auto skip = (size - st::reactionInlineImage) / 2;
const auto tags = areTags();
const auto inbubble = (_data.flags & Data::Flag::InBubble);
const auto flipped = (_data.flags & Data::Flag::Flipped);
p.setFont(tags ? st::reactionInlineTagFont : st::semiboldFont);
for (const auto &button : _buttons) {
if (context.reactionInfo
&& button.animation
&& button.animation->finished()) {
// Let the animation (and its custom emoji) live while painting.
finished.push_back(std::move(button.animation));
}
const auto animating = (button.animation != nullptr);
const auto &geometry = button.geometry;
const auto mine = button.chosen;
const auto withoutMine = button.count - (mine ? 1 : 0);
const auto skipImage = animating
&& (withoutMine < 1 || !button.animation->flying());
const auto bubbleProgress = skipImage
? button.animation->flyingProgress()
: 1.;
const auto bubbleReady = (bubbleProgress == 1.);
const auto bubbleSkip = anim::interpolate(
geometry.height() - geometry.width(),
0,
bubbleProgress);
const auto inner = geometry.marginsRemoved(padding);
const auto chosen = mine
&& (!animating || !button.animation->flying() || skipImage);
if (bubbleProgress > 0.) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
auto opacity = 1.;
auto color = QColor();
if (inbubble) {
if (!chosen) {
opacity = bubbleProgress * (context.outbg
? kOutNonChosenOpacity
: kInNonChosenOpacity);
} else if (!bubbleReady) {
opacity = bubbleProgress;
}
color = button.paid
? st->creditsBg3()->c
: stm->msgFileBg->c;
} else {
if (!bubbleReady) {
opacity = bubbleProgress;
}
color = (!chosen
? st->msgServiceBg()
: button.paid
? st->creditsBg2()
: st->msgServiceFg())->c;
}
const auto fill = geometry.marginsAdded({
flipped ? bubbleSkip : 0,
0,
flipped ? 0 : bubbleSkip,
0,
});
paintSingleBg(p, fill, color, opacity);
if (inbubble && !chosen) {
p.setOpacity(bubbleProgress);
}
}
if (!button.custom && button.image.isNull()) {
button.image = _owner->resolveReactionImageFor(button.id);
}
const auto textFg = !inbubble
? (chosen
? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c))
: st->msgServiceFg())
: !chosen
? (button.paid ? st->creditsFg() : stm->msgServiceFg)
: context.outbg
? (context.selected()
? st->historyFileOutIconFgSelected()
: st->historyFileOutIconFg())
: (context.selected()
? st->historyFileInIconFgSelected()
: st->historyFileInIconFg());
const auto image = QRect(
inner.topLeft() + QPoint(skip, skip),
QSize(st::reactionInlineImage, st::reactionInlineImage));
if (!skipImage) {
if (const auto custom = button.custom.get()) {
paintCustomFrame(
p,
custom,
inner.topLeft(),
context,
textFg.color());
} else if (!button.image.isNull()) {
p.drawImage(image.topLeft(), button.image);
}
}
if (animating) {
animations.push_back({
.animation = button.animation.get(),
.textColor = textFg.color(),
.target = image,
});
}
if ((tags && !button.textWidth) || bubbleProgress == 0.) {
p.setOpacity(1.);
continue;
}
resolveUserpicsImage(button);
const auto left = inner.x() + (flipped ? 0 : bubbleSkip);
if (button.userpics) {
p.drawImage(
left + size + st::reactionInlineUserpicsPadding.left(),
geometry.y() + st::reactionInlineUserpicsPadding.top(),
button.userpics->image);
} else {
p.setPen(textFg);
const auto textLeft = tags
? (left
- padding.left()
+ st::reactionInlineTagNamePosition.x())
: (left + size + st::reactionInlineSkip);
const auto textTop = geometry.y()
+ (tags
? st::reactionInlineTagNamePosition.y()
: ((geometry.height() - st::semiboldFont->height) / 2));
const auto font = tags
? st::reactionInlineTagFont
: st::semiboldFont;
p.drawText(textLeft, textTop + font->ascent, button.text);
}
if (!bubbleReady) {
p.setOpacity(1.);
}
}
if (!animations.empty()) {
const auto now = context.now;
context.reactionInfo->effectPaint = [
now,
list = std::move(animations)
](QPainter &p) {
auto result = QRect();
for (const auto &single : list) {
const auto area = single.animation->paintGetArea(
p,
QPoint(),
single.target,
single.textColor,
QRect(), // Clip, for emoji status.
now);
result = result.isEmpty() ? area : result.united(area);
}
return result;
};
}
}
float64 InlineList::TagDotAlpha() {
return 0.6;
}
QImage InlineList::PrepareTagBg(QColor tagBg, QColor dotBg) {
const auto padding = st::reactionInlinePadding;
const auto size = st::reactionInlineSize;
const auto width = padding.left()
+ size
+ st::reactionInlineTagSkip
+ padding.right();
const auto height = padding.top() + size + padding.bottom();
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
QSize(width, height) * ratio,
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
result.fill(Qt::transparent);
auto p = QPainter(&result);
auto path = QPainterPath();
const auto arrow = st::reactionInlineTagArrow;
const auto rradius = st::reactionInlineTagRightRadius * 1.;
const auto radius = st::reactionInlineTagLeftRadius - rradius;
auto pen = QPen(tagBg);
pen.setWidthF(rradius * 2.);
pen.setJoinStyle(Qt::RoundJoin);
const auto rect = QRectF(0, 0, width, height).marginsRemoved(
{ rradius, rradius, rradius, rradius });
const auto right = rect.x() + rect.width();
const auto bottom = rect.y() + rect.height();
path.moveTo(rect.x() + radius, rect.y());
path.lineTo(right - arrow, rect.y());
path.lineTo(right, rect.y() + rect.height() / 2);
path.lineTo(right - arrow, bottom);
path.lineTo(rect.x() + radius, bottom);
path.arcTo(
QRectF(rect.x(), bottom - radius * 2, radius * 2, radius * 2),
270,
-90);
path.lineTo(rect.x(), rect.y() + radius);
path.arcTo(
QRectF(rect.x(), rect.y(), radius * 2, radius * 2),
180,
-90);
path.closeSubpath();
const auto dsize = st::reactionInlineTagDot;
const auto dot = QRectF(
right - st::reactionInlineTagDotSkip - dsize,
rect.y() + (rect.height() - dsize) / 2.,
dsize,
dsize);
auto hq = PainterHighQualityEnabler(p);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.setPen(pen);
p.setBrush(tagBg);
p.drawPath(path);
if (dotBg.alpha() > 0) {
p.setPen(Qt::NoPen);
p.setBrush(dotBg);
p.drawEllipse(dot);
}
p.end();
return result;
}
void InlineList::validateTagBg(const QColor &color) const {
if (!_tagBg.isNull() && _tagBgColor == color) {
return;
}
_tagBgColor = color;
auto dot = color;
dot.setAlphaF(dot.alphaF() * TagDotAlpha());
_tagBg = PrepareTagBg(color, anim::with_alpha(color, TagDotAlpha()));
}
void InlineList::paintSingleBg(
Painter &p,
const QRect &fill,
const QColor &color,
float64 opacity) const {
p.setOpacity(opacity);
if (!areTags()) {
const auto radius = fill.height() / 2.;
p.setBrush(color);
p.drawRoundedRect(fill, radius, radius);
return;
}
validateTagBg(color);
const auto ratio = style::DevicePixelRatio();
const auto left = st::reactionInlineTagLeftRadius;
const auto right = (_tagBg.width() / ratio) - left;
Assert(right > 0);
const auto useLeft = std::min(fill.width(), left);
p.drawImage(
QRect(fill.x(), fill.y(), useLeft, fill.height()),
_tagBg,
QRect(0, 0, useLeft * ratio, _tagBg.height()));
const auto middle = fill.width() - left - right;
if (middle > 0) {
p.fillRect(fill.x() + left, fill.y(), middle, fill.height(), color);
}
if (const auto useRight = fill.width() - left; useRight > 0) {
p.drawImage(
QRect(
fill.x() + fill.width() - useRight,
fill.y(),
useRight,
fill.height()),
_tagBg,
QRect(_tagBg.width() - useRight * ratio,
0,
useRight * ratio,
_tagBg.height()));
}
}
bool InlineList::getState(
QPoint point,
not_null<TextState*> outResult) const {
const auto dimension = countDimension(width());
const auto left = dimension.left;
if (!QRect(left, 0, dimension.width, height()).contains(point)) {
return false;
}
for (const auto &button : _buttons) {
if (button.geometry.contains(point)) {
if (!button.link) {
button.link = _handlerFactory(button.id);
button.link->setProperty(
kReactionsCountEmojiProperty,
QVariant::fromValue(button.id));
_owner->preloadAnimationsFor(button.id);
}
outResult->link = button.link;
return true;
}
}
return false;
}
void InlineList::animate(
Ui::ReactionFlyAnimationArgs &&args,
Fn<void()> repaint) {
const auto i = ranges::find(_buttons, args.id, &Button::id);
if (i == end(_buttons)) {
return;
}
i->animation = std::make_unique<Ui::ReactionFlyAnimation>(
_owner,
std::move(args),
std::move(repaint),
st::reactionInlineImage);
}
void InlineList::resolveUserpicsImage(const Button &button) const {
const auto userpics = button.userpics.get();
const auto regenerate = [&] {
if (!userpics) {
return false;
} else if (userpics->image.isNull()) {
return true;
}
for (auto &entry : userpics->list) {
const auto peer = entry.peer;
auto &view = entry.view;
const auto wasView = view.cloud.get();
if (peer->userpicUniqueKey(view) != entry.uniqueKey
|| view.cloud.get() != wasView) {
return true;
}
}
return false;
}();
if (!regenerate) {
return;
}
GenerateUserpicsInRow(
userpics->image,
userpics->list,
st::reactionInlineUserpics,
kMaxRecentUserpics);
}
void InlineList::paintCustomFrame(
Painter &p,
not_null<Ui::Text::CustomEmoji*> emoji,
QPoint innerTopLeft,
const PaintContext &context,
const QColor &textColor) const {
if (_customCache.isNull()) {
using namespace Ui::Text;
const auto size = st::emojiSize;
const auto factor = style::DevicePixelRatio();
const auto adjusted = AdjustCustomEmojiSize(size);
_customCache = QImage(
QSize(adjusted, adjusted) * factor,
QImage::Format_ARGB32_Premultiplied);
_customCache.setDevicePixelRatio(factor);
_customSkip = (size - adjusted) / 2;
}
_customCache.fill(Qt::transparent);
auto q = QPainter(&_customCache);
emoji->paint(q, {
.textColor = textColor,
.now = context.now,
.paused = context.paused || On(PowerSaving::kEmojiChat),
});
q.end();
_customCache = Images::Round(
std::move(_customCache),
(Images::Option::RoundLarge
| Images::Option::RoundSkipTopRight
| Images::Option::RoundSkipBottomRight));
p.drawImage(
innerTopLeft + QPoint(_customSkip, _customSkip),
_customCache);
}
auto InlineList::takeAnimations()
-> base::flat_map<ReactionId, std::unique_ptr<Ui::ReactionFlyAnimation>> {
auto result = base::flat_map<
ReactionId,
std::unique_ptr<Ui::ReactionFlyAnimation>>();
for (auto &button : _buttons) {
if (button.animation) {
result.emplace(button.id, std::move(button.animation));
}
}
return result;
}
void InlineList::continueAnimations(base::flat_map<
ReactionId,
std::unique_ptr<Ui::ReactionFlyAnimation>> animations) {
for (auto &[id, animation] : animations) {
const auto i = ranges::find(_buttons, id, &Button::id);
if (i != end(_buttons)) {
i->animation = std::move(animation);
}
}
}
InlineListData InlineListDataFromMessage(not_null<Element*> view) {
using Flag = InlineListData::Flag;
const auto item = view->data();
auto result = InlineListData();
result.reactions = item->reactionsWithLocal();
const auto shouldAddEmptyPaidButton = [&] {
if (view->context() == Context::ChatPreview) {
return false;
}
if (result.reactions.empty()) {
return false;
}
const auto hasPaidReaction = ranges::any_of(
result.reactions,
[](const MessageReaction &r) { return r.id.paid(); });
if (hasPaidReaction) {
return false;
}
if (const auto channel = item->history()->peer->asChannel()) {
return channel->allowedReactions().paidEnabled;
} else if (const auto chat = item->history()->peer->asChat()) {
return chat->allowedReactions().paidEnabled;
}
return false;
}();
if (shouldAddEmptyPaidButton) {
result.reactions.insert(
result.reactions.begin(),
MessageReaction{ .id = ReactionId::Paid(), .count = 0 });
}
if (const auto user = item->history()->peer->asUser()) {
// Always show userpics, we have all information.
result.recent.reserve(result.reactions.size());
const auto self = user->session().user();
for (const auto &reaction : result.reactions) {
auto &list = result.recent[reaction.id];
list.reserve(reaction.count);
if (!reaction.my || reaction.count > 1) {
list.push_back(user);
}
if (reaction.my) {
list.push_back(self);
}
}
} else {
const auto &recent = item->recentReactions();
const auto showUserpics = [&] {
if (recent.size() != result.reactions.size()) {
return false;
}
auto sum = 0;
for (const auto &reaction : result.reactions) {
if ((sum += reaction.count) > kMaxRecentUserpics) {
return false;
}
const auto i = recent.find(reaction.id);
if (i == end(recent) || reaction.count != i->second.size()) {
return false;
}
}
return true;
}();
if (showUserpics) {
result.recent.reserve(recent.size());
for (const auto &[id, list] : recent) {
result.recent.emplace(id).first->second = list
| ranges::views::transform(&Data::RecentReaction::peer)
| ranges::to_vector;
}
}
}
result.flags = (view->hasOutLayout() ? Flag::OutLayout : Flag())
| (view->embedReactionsInBubble() ? Flag::InBubble : Flag())
| (item->reactionsAreTags() ? Flag::Tags : Flag())
| (item->isService() ? Flag::Centered : Flag());
return result;
}
ReactionId ReactionIdOfLink(const ClickHandlerPtr &link) {
return link
? link->property(kReactionsCountEmojiProperty).value<ReactionId>()
: ReactionId();
}
ReactionCount ReactionCountOfLink(
HistoryItem *item,
const ClickHandlerPtr &link) {
const auto id = ReactionIdOfLink(link);
if (!item || !id) {
return {};
}
const auto groups = &item->history()->owner().groups();
if (const auto group = groups->find(item)) {
item = group->items.front();
}
const auto &list = item->reactions();
const auto i = ranges::find(list, id, &Data::MessageReaction::id);
if (i == end(list) || !i->count) {
return {};
}
const auto formatted = Lang::FormatCountToShort(i->count);
return { .count = i->count, .shortened = formatted.shortened };
}
} // namespace HistoryView::Reactions

View File

@@ -0,0 +1,168 @@
/*
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 "history/view/history_view_object.h"
#include "data/data_message_reaction_id.h"
namespace Data {
class Reactions;
} // namespace Data
namespace Ui {
struct ChatPaintContext;
struct ReactionFlyAnimationArgs;
class ReactionFlyAnimation;
} // namespace Ui
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
class Element;
struct TextState;
struct UserpicInRow;
} // namespace HistoryView
namespace HistoryView::Reactions {
using ::Data::ReactionId;
using ::Data::MessageReaction;
struct InlineListData {
enum class Flag : uchar {
InBubble = 0x01,
OutLayout = 0x02,
Flipped = 0x04,
Tags = 0x08,
Centered = 0x10,
};
friend inline constexpr bool is_flag_type(Flag) { return true; };
using Flags = base::flags<Flag>;
std::vector<MessageReaction> reactions;
base::flat_map<ReactionId, std::vector<not_null<PeerData*>>> recent;
Flags flags = {};
};
class InlineList final : public Object {
public:
using Data = InlineListData;
InlineList(
not_null<::Data::Reactions*> owner,
Fn<ClickHandlerPtr(ReactionId)> handlerFactory,
Fn<void()> customEmojiRepaint,
Data &&data);
~InlineList();
void update(Data &&data, int availableWidth);
QSize countCurrentSize(int newWidth) override;
[[nodiscard]] int countNiceWidth() const;
[[nodiscard]] int placeAndResizeGetHeight(QRect available);
void flipToRight();
void updateSkipBlock(int width, int height);
void removeSkipBlock();
[[nodiscard]] bool areTags() const;
[[nodiscard]] std::vector<ReactionId> computeTagsList() const;
[[nodiscard]] bool hasCustomEmoji() const;
void unloadCustomEmoji();
void paint(
Painter &p,
const PaintContext &context,
int outerWidth,
const QRect &clip) const;
[[nodiscard]] bool getState(
QPoint point,
not_null<TextState*> outResult) const;
void animate(
Ui::ReactionFlyAnimationArgs &&args,
Fn<void()> repaint);
[[nodiscard]] auto takeAnimations()
-> base::flat_map<
ReactionId,
std::unique_ptr<Ui::ReactionFlyAnimation>>;
void continueAnimations(base::flat_map<
ReactionId,
std::unique_ptr<Ui::ReactionFlyAnimation>> animations);
[[nodiscard]] static float64 TagDotAlpha();
[[nodiscard]] static QImage PrepareTagBg(QColor tagBg, QColor dotBg);
private:
struct Dimension {
int left = 0;
int width = 0;
};
struct Userpics {
QImage image;
std::vector<UserpicInRow> list;
bool someNotLoaded = false;
};
struct Button;
void layout();
void layoutButtons();
void setButtonTag(Button &button, const QString &title);
void setButtonCount(Button &button, int count);
void setButtonUserpics(
Button &button,
const std::vector<not_null<PeerData*>> &peers);
[[nodiscard]] Button prepareButtonWithId(const ReactionId &id);
void resolveUserpicsImage(const Button &button) const;
void paintCustomFrame(
Painter &p,
not_null<Ui::Text::CustomEmoji*> emoji,
QPoint innerTopLeft,
const PaintContext &context,
const QColor &textColor) const;
void paintSingleBg(
Painter &p,
const QRect &fill,
const QColor &color,
float64 opacity) const;
void validateTagBg(const QColor &color) const;
QSize countOptimalSize() override;
[[nodiscard]] Dimension countDimension(int width) const;
const not_null<::Data::Reactions*> _owner;
const Fn<ClickHandlerPtr(ReactionId)> _handlerFactory;
const Fn<void()> _customEmojiRepaint;
Data _data;
std::vector<Button> _buttons;
QSize _skipBlock;
mutable QImage _tagBg;
mutable QColor _tagBgColor;
mutable QImage _customCache;
mutable int _customSkip = 0;
bool _hasCustomEmoji = false;
};
[[nodiscard]] InlineListData InlineListDataFromMessage(
not_null<Element*> view);
[[nodiscard]] ReactionId ReactionIdOfLink(const ClickHandlerPtr &link);
struct ReactionCount {
int count = 0;
bool shortened = false;
};
[[nodiscard]] ReactionCount ReactionCountOfLink(
HistoryItem *item,
const ClickHandlerPtr &link);
} // namespace HistoryView::Reactions

View File

@@ -0,0 +1,924 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/reactions/history_view_reactions_button.h"
#include "history/view/history_view_cursor_state.h"
#include "history/history_item.h"
#include "history/history.h"
#include "ui/chat/chat_style.h"
#include "ui/widgets/popup_menu.h"
#include "ui/ui_utility.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "data/data_peer_values.h"
#include "data/data_peer.h"
#include "data/data_message_reactions.h"
#include "lang/lang_keys.h"
#include "core/click_handler_types.h"
#include "main/main_session.h"
#include "base/event_filter.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"
namespace HistoryView::Reactions {
namespace {
constexpr auto kToggleDuration = crl::time(120);
constexpr auto kActivateDuration = crl::time(150);
constexpr auto kExpandDuration = crl::time(300);
constexpr auto kCollapseDuration = crl::time(250);
constexpr auto kButtonShowDelay = crl::time(300);
constexpr auto kButtonExpandDelay = crl::time(25);
constexpr auto kButtonHideDelay = crl::time(300);
constexpr auto kButtonExpandedHideDelay = crl::time(0);
constexpr auto kMaxReactionsScrollAtOnce = 2;
constexpr auto kRefreshListDelay = crl::time(100);
[[nodiscard]] QPoint LocalPosition(not_null<QWheelEvent*> e) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
return e->position().toPoint();
#else // Qt >= 6.0
return e->pos();
#endif // Qt >= 6.0
}
[[nodiscard]] QSize CountMaxSizeWithMargins(style::margins margins) {
return QRect(
QPoint(),
st::reactionCornerSize
).marginsAdded(margins).size();
}
[[nodiscard]] QSize CountOuterSize() {
return CountMaxSizeWithMargins(st::reactionCornerShadow);
}
} // namespace
Button::Button(
Fn<void(QRect)> update,
ButtonParameters parameters,
Fn<void()> hide)
: _update(std::move(update))
, _finalScale(ScaleForState(_state))
, _collapsed(QPoint(), CountOuterSize())
, _finalHeight(_collapsed.height())
, _expandTimer([=] { applyState(State::Inside, _update); })
, _hideTimer(hide) {
applyParameters(parameters, nullptr);
}
Button::~Button() = default;
bool Button::isHidden() const {
return (_state == State::Hidden) && !_opacityAnimation.animating();
}
QRect Button::geometry() const {
return _geometry;
}
int Button::expandedHeight() const {
return _expandedHeight;
}
int Button::scroll() const {
return _scroll;
}
int Button::scrollMax() const {
return _expandedInnerHeight - _expandedHeight;
}
float64 Button::expandAnimationOpacity(float64 expandRatio) const {
return (_collapseType == CollapseType::Fade)
? expandRatio
: 1.;
}
int Button::expandAnimationScroll(float64 expandRatio) const {
return (_collapseType == CollapseType::Scroll && expandRatio < 1.)
? std::clamp(int(base::SafeRound(expandRatio * _scroll)), 0, _scroll)
: _scroll;
}
bool Button::expandUp() const {
return (_expandDirection == ExpandDirection::Up);
}
bool Button::consumeWheelEvent(not_null<QWheelEvent*> e) {
const auto scrollMax = (_expandedInnerHeight - _expandedHeight);
if (_state != State::Inside
|| scrollMax <= 0
|| !_geometry.contains(LocalPosition(e))) {
return false;
}
const auto delta = e->angleDelta();
const auto horizontal = std::abs(delta.x()) > std::abs(delta.y());
if (horizontal) {
return false;
}
const auto between = st::reactionCornerSkip;
const auto oneHeight = (st::reactionCornerSize.height() + between);
const auto max = oneHeight * kMaxReactionsScrollAtOnce;
const auto shift = std::clamp(
delta.y() * (expandUp() ? 1 : -1),
-max,
max);
_scroll = std::clamp(_scroll + shift, 0, scrollMax);
_update(_geometry);
e->accept();
return true;
}
void Button::applyParameters(ButtonParameters parameters) {
applyParameters(std::move(parameters), _update);
}
void Button::applyParameters(
ButtonParameters parameters,
Fn<void(QRect)> update) {
const auto shift = parameters.center - _collapsed.center();
_collapsed = _collapsed.translated(shift);
updateGeometry(update);
const auto inner = _geometry.marginsRemoved(st::reactionCornerShadow);
const auto active = inner.marginsAdded(
st::reactionCornerActiveAreaPadding
).contains(parameters.pointer);
const auto inside = inner.contains(parameters.pointer)
|| (active && (_state == State::Inside));
if (_state != State::Inside && !_heightAnimation.animating()) {
updateExpandDirection(parameters);
}
const auto delayInside = inside && (_state != State::Inside);
if (!delayInside) {
_expandTimer.cancel();
_lastGlobalPosition = std::nullopt;
} else {
const auto globalPositionChanged = _lastGlobalPosition
&& (*_lastGlobalPosition != parameters.globalPointer);
if (globalPositionChanged || _state == State::Hidden) {
_expandTimer.callOnce(kButtonExpandDelay);
}
_lastGlobalPosition = parameters.globalPointer;
}
const auto wasInside = (_state == State::Inside);
const auto state = (inside && !delayInside)
? State::Inside
: active
? State::Active
: State::Shown;
applyState(state, update);
if (parameters.outside && _state == State::Shown) {
_hideTimer.callOnce(wasInside
? kButtonExpandedHideDelay
: kButtonHideDelay);
} else {
_hideTimer.cancel();
}
}
void Button::updateExpandDirection(const ButtonParameters &parameters) {
const auto maxAddedHeight = (parameters.reactionsCount - 1)
* (st::reactionCornerSize.height() + st::reactionCornerSkip)
+ (parameters.reactionsCount > 1 ? 2 * st::reactionExpandedSkip : 0);
_expandedInnerHeight = _collapsed.height() + maxAddedHeight;
const auto addedHeight = std::min(
maxAddedHeight,
st::reactionCornerAddedHeightMax);
_expandedHeight = _collapsed.height() + addedHeight;
_scroll = std::clamp(_scroll, 0, scrollMax());
if (parameters.reactionsCount < 2) {
return;
}
const auto up = (_collapsed.y() - addedHeight >= parameters.visibleTop)
|| (_collapsed.y() + _collapsed.height() + addedHeight
> parameters.visibleBottom);
_expandDirection = up ? ExpandDirection::Up : ExpandDirection::Down;
}
void Button::updateGeometry(Fn<void(QRect)> update) {
const auto added = int(base::SafeRound(
_heightAnimation.value(_finalHeight)
)) - _collapsed.height();
if (!added && _state != State::Inside) {
_scroll = 0;
}
const auto geometry = _collapsed.marginsAdded({
0,
(_expandDirection == ExpandDirection::Up) ? added : 0,
0,
(_expandDirection == ExpandDirection::Down) ? added : 0,
});
if (_geometry != geometry) {
if (update) {
update(_geometry);
}
_geometry = geometry;
if (update) {
update(_geometry);
}
}
}
void Button::applyState(State state) {
applyState(state, _update);
}
void Button::applyState(State state, Fn<void(QRect)> update) {
if (state == State::Hidden) {
_expandTimer.cancel();
_hideTimer.cancel();
}
const auto finalHeight = (state == State::Hidden)
? _heightAnimation.value(_finalHeight)
: (state == State::Inside)
? _expandedHeight
: _collapsed.height();
if (_finalHeight != finalHeight) {
if (state == State::Hidden) {
_heightAnimation.stop();
} else {
if (!_heightAnimation.animating()) {
_collapseType = (_scroll < st::reactionCollapseFadeThreshold)
? CollapseType::Scroll
: CollapseType::Fade;
}
_heightAnimation.start(
[=] { updateGeometry(_update); },
_finalHeight,
finalHeight,
(state == State::Inside
? kExpandDuration
: kCollapseDuration),
anim::easeOutCirc);
}
_finalHeight = finalHeight;
}
updateGeometry(update);
if (_state == state) {
return;
}
const auto duration = (state == State::Hidden || _state == State::Hidden)
? kToggleDuration
: kActivateDuration;
const auto finalScale = ScaleForState(state);
_opacityAnimation.start(
[=] { _update(_geometry); },
OpacityForScale(ScaleForState(_state)),
OpacityForScale(ScaleForState(state)),
duration,
anim::sineInOut);
if (state != State::Hidden && _finalScale != finalScale) {
_scaleAnimation.start(
[=] { _update(_geometry); },
_finalScale,
finalScale,
duration,
anim::sineInOut);
_finalScale = finalScale;
}
_state = state;
}
float64 Button::ScaleForState(State state) {
switch (state) {
case State::Hidden: return 1. / 3;
case State::Shown: return 2. / 3;
case State::Active:
case State::Inside: return 1.;
}
Unexpected("State in ReactionButton::ScaleForState.");
}
float64 Button::OpacityForScale(float64 scale) {
return std::min(
((scale - ScaleForState(State::Hidden))
/ (ScaleForState(State::Shown) - ScaleForState(State::Hidden))),
1.);
}
float64 Button::currentScale() const {
return _scaleAnimation.value(_finalScale);
}
float64 Button::currentOpacity() const {
return _opacityAnimation.value(OpacityForScale(ScaleForState(_state)));
}
Manager::Manager(
QWidget *wheelEventsTarget,
Fn<void(QRect)> buttonUpdate,
IconFactory iconFactory)
: _outer(CountOuterSize())
, _inner(QRect({}, st::reactionCornerSize))
, _strip(
st::reactPanelEmojiPan,
_inner,
st::reactionCornerImage,
crl::guard(this, [=] { updateCurrentButton(); }),
std::move(iconFactory))
, _cachedRound(
st::reactionCornerSize,
st::reactionCornerShadow,
_inner.width())
, _buttonShowTimer([=] { showButtonDelayed(); })
, _buttonUpdate(std::move(buttonUpdate)) {
_inner.translate(QRect({}, _outer).center() - _inner.center());
_expandedBuffer = _cachedRound.PrepareImage(QSize(
_outer.width(),
_outer.height() + st::reactionCornerAddedHeightMax));
if (wheelEventsTarget) {
stealWheelEvents(wheelEventsTarget);
}
_createChooseCallback = [=](ReactionId id) {
return [=] {
if (auto chosen = lookupChosen(id)) {
updateButton({});
_chosen.fire(std::move(chosen));
}
};
};
}
Manager::~Manager() = default;
ChosenReaction Manager::lookupChosen(const ReactionId &id) const {
auto result = ChosenReaction{
.context = _buttonContext,
.id = id,
};
const auto button = _button.get();
if (!button) {
return result;
}
const auto index = _strip.fillChosenIconGetIndex(result);
if (result.icon.isNull()) {
return result;
}
const auto between = st::reactionCornerSkip;
const auto oneHeight = (st::reactionCornerSize.height() + between);
const auto expanded = (_strip.count() > 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 = _strip.computeOverSize();
result.localGeometry = QRect(
rect.x() + (rect.width() - imageSize) / 2,
rect.y() + (rect.height() - imageSize) / 2,
imageSize,
imageSize);
return result;
}
void Manager::stealWheelEvents(not_null<QWidget*> target) {
base::install_event_filter(target, [=](not_null<QEvent*> e) {
if (e->type() != QEvent::Wheel
|| !consumeWheelEvent(static_cast<QWheelEvent*>(e.get()))) {
return base::EventFilterResult::Continue;
}
Ui::SendSynteticMouseEvent(target, QEvent::MouseMove, Qt::NoButton);
return base::EventFilterResult::Cancel;
});
}
void Manager::updateButton(ButtonParameters parameters) {
if (parameters.cursorLeft && _menu) {
return;
}
const auto contextChanged = (_buttonContext != parameters.context);
if (contextChanged) {
_strip.setSelected(-1);
if (_button) {
_button->applyState(ButtonState::Hidden);
_buttonHiding.push_back(std::move(_button));
}
_buttonShowTimer.cancel();
_scheduledParameters = std::nullopt;
}
_buttonContext = parameters.context;
parameters.reactionsCount = _strip.count();
if (!_buttonContext || !parameters.reactionsCount) {
return;
} else if (_button) {
_button->applyParameters(parameters);
if (_button->geometry().height() == _outer.height()) {
clearAppearAnimations();
}
return;
} else if (parameters.outside) {
_buttonShowTimer.cancel();
_scheduledParameters = std::nullopt;
return;
}
const auto globalPositionChanged = _scheduledParameters
&& (_scheduledParameters->globalPointer != parameters.globalPointer);
const auto positionChanged = _scheduledParameters
&& (_scheduledParameters->pointer != parameters.pointer);
_scheduledParameters = parameters;
if ((_buttonShowTimer.isActive() && positionChanged)
|| globalPositionChanged) {
_buttonShowTimer.callOnce(kButtonShowDelay);
}
}
void Manager::showButtonDelayed() {
clearAppearAnimations();
_button = std::make_unique<Button>(
_buttonUpdate,
*_scheduledParameters,
[=]{ updateButton({}); });
}
void Manager::applyList(const Data::PossibleItemReactionsRef &reactions) {
using Button = Strip::AddedButton;
_strip.applyList(
reactions.recent,
(/*reactions.customAllowed
? Button::Expand
: */Button::None));
_tagsStrip = reactions.tags;
}
QMargins Manager::innerMargins() const {
return {
_inner.x(),
_inner.y(),
_outer.width() - _inner.x() - _inner.width(),
_outer.height() - _inner.y() - _inner.height(),
};
}
QRect Manager::buttonInner() const {
return buttonInner(_button.get());
}
QRect Manager::buttonInner(not_null<Button*> button) const {
return button->geometry().marginsRemoved(innerMargins());
}
void Manager::updateCurrentButton() const {
if (const auto button = _button.get()) {
_buttonUpdate(button->geometry());
}
}
void Manager::removeStaleButtons() {
_buttonHiding.erase(
ranges::remove_if(_buttonHiding, &Button::isHidden),
end(_buttonHiding));
}
void Manager::paint(QPainter &p, const PaintContext &context) {
removeStaleButtons();
for (const auto &button : _buttonHiding) {
paintButton(p, context, button.get());
}
if (const auto current = _button.get()) {
if (context.gestureHorizontal.ratio) {
current->applyState(ButtonState::Hidden);
_buttonHiding.push_back(std::move(_button));
}
paintButton(p, context, current);
}
for (const auto &[id, effect] : _collectedEffects) {
const auto offset = effect.effectOffset;
p.translate(offset);
_activeEffectAreas[id] = effect.effectPaint(p).translated(offset);
p.translate(-offset);
}
_collectedEffects.clear();
}
ClickHandlerPtr Manager::computeButtonLink(QPoint position) const {
if (_strip.empty()) {
_strip.setSelected(-1);
return nullptr;
}
const auto inner = buttonInner();
const auto top = _button->expandUp()
? (inner.y() + inner.height() - position.y())
: (position.y() - inner.y());
const auto shifted = top + _button->scroll();
const auto between = st::reactionCornerSkip;
const auto oneHeight = (st::reactionCornerSize.height() + between);
const auto index = std::clamp(
int(base::SafeRound(shifted + between / 2.)) / oneHeight,
0,
int(_strip.count() - 1));
_strip.setSelected(index);
const auto selected = _strip.selected();
if (selected == Strip::AddedButton::Expand) {
if (!_expandLink) {
_expandLink = std::make_shared<LambdaClickHandler>([=] {
_expandChosen.fire_copy(_buttonContext);
});
}
return _expandLink;
}
const auto id = std::get_if<ReactionId>(&selected);
if (!id || id->empty()) {
return nullptr;
}
auto &result = _links[*id];
if (!result) {
result = resolveButtonLink(*id);
}
return result;
}
ClickHandlerPtr Manager::resolveButtonLink(const ReactionId &id) const {
const auto i = _reactionsLinks.find(id);
if (i != end(_reactionsLinks)) {
return i->second;
}
auto handler = std::make_shared<LambdaClickHandler>(
crl::guard(this, _createChooseCallback(id)));
handler->setProperty(
kSendReactionEmojiProperty,
QVariant::fromValue(id));
return _reactionsLinks.emplace(id, std::move(handler)).first->second;
}
TextState Manager::buttonTextState(QPoint position) const {
if (overCurrentButton(position)) {
auto result = TextState(nullptr, computeButtonLink(position));
result.itemId = _buttonContext;
return result;
} else {
_strip.setSelected(-1);
}
return {};
}
bool Manager::overCurrentButton(QPoint position) const {
if (!_button) {
return false;
}
return _button && buttonInner().contains(position);
}
void Manager::remove(FullMsgId context) {
_activeEffectAreas.remove(context);
if (_buttonContext == context) {
_buttonContext = {};
_button = nullptr;
}
}
bool Manager::consumeWheelEvent(not_null<QWheelEvent*> e) {
return _button && _button->consumeWheelEvent(e);
}
void Manager::paintButton(
QPainter &p,
const PaintContext &context,
not_null<Button*> button) {
const auto geometry = button->geometry();
if (!context.clip.intersects(geometry)) {
return;
}
constexpr auto kFramesCount = Ui::RoundAreaWithShadow::kFramesCount;
const auto scale = button->currentScale();
const auto scaleMin = Button::ScaleForState(ButtonState::Hidden);
const auto scaleMax = Button::ScaleForState(ButtonState::Active);
const auto progress = (scale - scaleMin) / (scaleMax - scaleMin);
const auto frame = int(base::SafeRound(progress * (kFramesCount - 1)));
const auto useScale = scaleMin
+ (frame / float64(kFramesCount - 1)) * (scaleMax - scaleMin);
paintButton(p, context, button, frame, useScale);
}
void Manager::paintButton(
QPainter &p,
const PaintContext &context,
not_null<Button*> button,
int frameIndex,
float64 scale) {
const auto opacity = button->currentOpacity();
if (opacity == 0.) {
return;
}
const auto geometry = button->geometry();
const auto position = geometry.topLeft();
const auto size = geometry.size();
const auto expanded = (size.height() - _outer.height());
if (opacity != 1.) {
p.setOpacity(opacity);
}
auto layeredPainter = std::optional<QPainter>();
if (expanded) {
_expandedBuffer.fill(Qt::transparent);
}
const auto q = expanded ? &layeredPainter.emplace(&_expandedBuffer) : &p;
const auto shadow = context.st->shadowFg()->c;
const auto background = context.st->windowBg()->c;
_cachedRound.setShadowColor(shadow);
_cachedRound.setBackgroundColor(background);
if (expanded) {
q->fillRect(QRect(QPoint(), size), context.st->windowBg());
} else {
const auto radius = _inner.height() / 2.;
const auto frame = _cachedRound.validateFrame(
frameIndex,
scale,
radius);
p.drawImage(position, *frame.image, frame.rect);
}
const auto current = (button == _button.get());
const auto expandRatio = expanded
? std::clamp(
float64(expanded) / (button->expandedHeight() - _outer.height()),
0.,
1.)
: 0.;
const auto expandedSkip = int(base::SafeRound(
expandRatio * st::reactionExpandedSkip));
const auto mainEmojiPosition = _inner.topLeft() + (!expanded
? position
: button->expandUp()
? QPoint(0, expanded - expandedSkip)
: QPoint(0, expandedSkip));
const auto mainEmoji = _strip.validateEmoji(frameIndex, scale);
if (expanded
|| (current && !_strip.onlyMainEmojiVisible())
|| _strip.onlyAddedButton()) {
const auto opacity = button->expandAnimationOpacity(expandRatio);
if (opacity != 1.) {
q->setOpacity(opacity);
}
const auto clip = QRect(
expanded ? QPoint() : position,
button->geometry().size()
).marginsRemoved(innerMargins());
const auto between = st::reactionCornerSkip;
const auto oneHeight = st::reactionCornerSize.height() + between;
const auto expandUp = button->expandUp();
const auto shift = QPoint(0, oneHeight * (expandUp ? -1 : 1));
const auto scroll = button->expandAnimationScroll(expandRatio);
const auto startEmojiPosition = mainEmojiPosition
+ QPoint(0, scroll * (expandUp ? 1 : -1));
_strip.paint(*q, startEmojiPosition, shift, clip, scale, !current);
if (opacity != 1.) {
q->setOpacity(1.);
}
if (current && expanded) {
_showingAll = true;
}
if (expanded) {
paintInnerGradients(*q, background, button, scroll, expandRatio);
}
if (opacity != 1.) {
const auto appearShift = st::reactionMainAppearShift * opacity;
const auto appearPosition = !expanded
? position
: button->expandUp()
? QPoint(0, expanded - appearShift)
: QPoint(0, appearShift);
q->setOpacity(1. - opacity);
q->drawImage(
appearPosition + _inner.topLeft(),
*mainEmoji.image,
mainEmoji.rect);
q->setOpacity(1.);
}
} else {
p.drawImage(mainEmojiPosition, *mainEmoji.image, mainEmoji.rect);
}
if (current && !expanded) {
clearAppearAnimations();
}
if (expanded) {
const auto radiusMin = _inner.height() / 2.;
const auto radiusMax = _inner.width() / 2.;
_cachedRound.overlayExpandedBorder(
*q,
size,
expandRatio,
radiusMin,
radiusMax,
scale);
layeredPainter.reset();
p.drawImage(
geometry,
_expandedBuffer,
QRect(QPoint(), size * style::DevicePixelRatio()));
}
if (opacity != 1.) {
p.setOpacity(1.);
}
}
void Manager::paintInnerGradients(
QPainter &p,
const QColor &background,
not_null<Button*> button,
int scroll,
float64 expandRatio) {
if (_gradientBackground != background) {
_gradientBackground = background;
_topGradient = _bottomGradient = QImage();
}
const auto endScroll = button->scrollMax() - scroll;
const auto size = st::reactionGradientSize;
const auto ensureGradient = [&](QImage &gradient, bool top) {
if (!gradient.isNull()) {
return;
}
gradient = Images::GenerateShadow(
size,
top ? 255 : 0,
top ? 0 : 255,
background);
};
ensureGradient(_topGradient, true);
ensureGradient(_bottomGradient, false);
const auto paintGradient = [&](QImage &gradient, int scrolled, int top) {
if (scrolled <= 0) {
return;
}
const auto opacity = (expandRatio * scrolled)
/ st::reactionGradientFadeSize;
p.setOpacity(opacity);
p.drawImage(
QRect(0, top, _outer.width(), size),
gradient,
QRect(QPoint(), gradient.size()));
};
const auto up = button->expandUp();
const auto start = st::reactionGradientStart;
paintGradient(_topGradient, up ? endScroll : scroll, start);
const auto bottomStart = button->geometry().height() - start - size;
paintGradient(_bottomGradient, up ? scroll : endScroll, bottomStart);
p.setOpacity(1.);
}
void Manager::clearAppearAnimations() {
if (!_showingAll) {
return;
}
_showingAll = false;
_strip.clearAppearAnimations();
}
std::optional<QRect> Manager::lookupEffectArea(FullMsgId itemId) const {
const auto i = _activeEffectAreas.find(itemId);
return (i != end(_activeEffectAreas))
? i->second
: std::optional<QRect>();
}
void Manager::startEffectsCollection() {
_collectedEffects.clear();
_currentReactionInfo = {};
}
auto Manager::currentReactionPaintInfo()
-> not_null<Ui::ReactionPaintInfo*> {
return &_currentReactionInfo;
}
void Manager::recordCurrentReactionEffect(FullMsgId itemId, QPoint origin) {
if (_currentReactionInfo.effectPaint) {
_currentReactionInfo.effectOffset += origin
+ _currentReactionInfo.position;
_collectedEffects[itemId] = base::take(_currentReactionInfo);
} else if (!_collectedEffects.empty()) {
_collectedEffects.remove(itemId);
}
}
bool Manager::showContextMenu(
QWidget *parent,
QContextMenuEvent *e,
const ReactionId &favorite) {
const auto selected = _strip.selected();
const auto id = std::get_if<ReactionId>(&selected);
if (!id || id->empty() || _tagsStrip) {
return false;
} else if (*id == favorite || id->paid()) {
return true;
}
_menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
_menu->addAction(
tr::lng_context_set_as_quick(tr::now),
[=, id = *id] { _faveRequests.fire_copy(id); },
&st::menuIconFave);
_menu->popup(e->globalPos());
return true;
}
auto Manager::faveRequests() const -> rpl::producer<ReactionId> {
return _faveRequests.events();
}
void SetupManagerList(
not_null<Manager*> manager,
rpl::producer<HistoryItem*> items) {
struct State {
PeerData *peer = nullptr;
HistoryItem *item = nullptr;
Main::Session *session = nullptr;
rpl::lifetime sessionLifetime;
rpl::lifetime peerLifetime;
base::Timer timer;
};
const auto state = manager->lifetime().make_state<State>();
std::move(
items
) | rpl::filter([=](HistoryItem *item) {
return (item != state->item);
}) | rpl::on_next([=](HistoryItem *item) {
state->item = item;
if (!item) {
return;
}
const auto peer = item->history()->peer;
const auto session = &peer->session();
const auto peerChanged = (state->peer != peer);
const auto sessionChanged = (state->session != session);
const auto push = [=] {
state->timer.cancel();
if (const auto item = state->item) {
manager->applyList(Data::LookupPossibleReactions(item));
}
};
state->timer.setCallback(push);
if (sessionChanged) {
state->sessionLifetime.destroy();
state->session = session;
Data::AmPremiumValue(
session
) | rpl::skip(
1
) | rpl::on_next(push, state->sessionLifetime);
session->changes().messageUpdates(
Data::MessageUpdate::Flag::Destroyed
) | rpl::on_next([=](const Data::MessageUpdate &update) {
if (update.item == state->item) {
state->item = nullptr;
state->timer.cancel();
}
}, state->sessionLifetime);
session->data().itemDataChanges(
) | rpl::filter([=](not_null<HistoryItem*> item) {
return (item == state->item);
}) | rpl::on_next(push, state->sessionLifetime);
const auto &reactions = session->data().reactions();
rpl::merge(
reactions.topUpdates(),
reactions.recentUpdates(),
reactions.defaultUpdates(),
reactions.favoriteUpdates(),
reactions.myTagsUpdates(),
reactions.tagsUpdates()
) | rpl::on_next([=] {
if (!state->timer.isActive()) {
state->timer.callOnce(kRefreshListDelay);
}
}, state->sessionLifetime);
}
if (peerChanged) {
state->peer = peer;
state->peerLifetime = rpl::combine(
Data::PeerAllowedReactionsValue(peer),
Data::UniqueReactionsLimitValue(peer)
) | rpl::on_next(push);
} else {
push();
}
}, manager->lifetime());
manager->faveRequests(
) | rpl::filter([=] {
return (state->session != nullptr);
}) | rpl::on_next([=](const Data::ReactionId &id) {
state->session->data().reactions().setFavorite(id);
manager->updateButton({});
}, manager->lifetime());
}
} // namespace HistoryView

View File

@@ -0,0 +1,260 @@
/*
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 "base/timer.h"
#include "base/unique_qptr.h"
#include "ui/effects/animations.h"
#include "ui/effects/round_area_with_shadow.h"
#include "history/view/reactions/history_view_reactions_strip.h"
#include "ui/chat/chat_style.h" // Ui::ReactionPaintInfo
namespace Ui {
struct ChatPaintContext;
struct ReactionPaintInfo;
class PopupMenu;
} // namespace Ui
namespace Data {
struct ReactionId;
struct Reaction;
struct PossibleItemReactionsRef;
class DocumentMedia;
} // namespace Data
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
struct TextState;
} // namespace HistoryView
namespace Main {
class Session;
} // namespace Main
namespace HistoryView::Reactions {
enum class ExpandDirection {
Up,
Down,
};
struct ButtonParameters {
[[nodiscard]] ButtonParameters translated(QPoint delta) const {
auto result = *this;
result.center += delta;
result.pointer += delta;
return result;
}
FullMsgId context;
QPoint center;
QPoint pointer;
QPoint globalPointer;
int reactionsCount = 1;
int visibleTop = 0;
int visibleBottom = 0;
bool outside = false;
bool cursorLeft = false;
};
enum class ButtonState {
Hidden,
Shown,
Active,
Inside,
};
class Button final {
public:
Button(
Fn<void(QRect)> update,
ButtonParameters parameters,
Fn<void()> hide);
~Button();
void applyParameters(ButtonParameters parameters);
using State = ButtonState;
void applyState(State state);
[[nodiscard]] bool expandUp() const;
[[nodiscard]] bool isHidden() const;
[[nodiscard]] QRect geometry() const;
[[nodiscard]] int expandedHeight() const;
[[nodiscard]] int scroll() const;
[[nodiscard]] int scrollMax() const;
[[nodiscard]] float64 currentScale() const;
[[nodiscard]] float64 currentOpacity() const;
[[nodiscard]] float64 expandAnimationOpacity(float64 expandRatio) const;
[[nodiscard]] int expandAnimationScroll(float64 expandRatio) const;
[[nodiscard]] bool consumeWheelEvent(not_null<QWheelEvent*> e);
[[nodiscard]] static float64 ScaleForState(State state);
[[nodiscard]] static float64 OpacityForScale(float64 scale);
private:
enum class CollapseType {
Scroll,
Fade,
};
void updateGeometry(Fn<void(QRect)> update);
void applyState(State satte, Fn<void(QRect)> update);
void applyParameters(
ButtonParameters parameters,
Fn<void(QRect)> update);
void updateExpandDirection(const ButtonParameters &parameters);
const Fn<void(QRect)> _update;
State _state = State::Hidden;
float64 _finalScale = 0.;
Ui::Animations::Simple _scaleAnimation;
Ui::Animations::Simple _opacityAnimation;
Ui::Animations::Simple _heightAnimation;
QRect _collapsed;
QRect _geometry;
int _expandedInnerHeight = 0;
int _expandedHeight = 0;
int _finalHeight = 0;
int _scroll = 0;
ExpandDirection _expandDirection = ExpandDirection::Up;
CollapseType _collapseType = CollapseType::Scroll;
base::Timer _expandTimer;
base::Timer _hideTimer;
std::optional<QPoint> _lastGlobalPosition;
};
class Manager final : public base::has_weak_ptr {
public:
Manager(
QWidget *wheelEventsTarget,
Fn<void(QRect)> buttonUpdate,
IconFactory iconFactory = nullptr);
~Manager();
using ReactionId = ::Data::ReactionId;
void applyList(const Data::PossibleItemReactionsRef &reactions);
void updateButton(ButtonParameters parameters);
void paint(QPainter &p, const PaintContext &context);
[[nodiscard]] TextState buttonTextState(QPoint position) const;
void remove(FullMsgId context);
[[nodiscard]] bool consumeWheelEvent(not_null<QWheelEvent*> e);
[[nodiscard]] rpl::producer<ChosenReaction> chosen() const {
return _chosen.events();
}
[[nodiscard]] rpl::producer<FullMsgId> expandChosen() const {
return _expandChosen.events();
}
[[nodiscard]] std::optional<QRect> lookupEffectArea(
FullMsgId itemId) const;
void startEffectsCollection();
[[nodiscard]] auto currentReactionPaintInfo()
-> not_null<Ui::ReactionPaintInfo*>;
void recordCurrentReactionEffect(FullMsgId itemId, QPoint origin);
bool showContextMenu(
QWidget *parent,
QContextMenuEvent *e,
const ReactionId &favorite);
[[nodiscard]] rpl::producer<ReactionId> faveRequests() const;
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private:
void showButtonDelayed();
void stealWheelEvents(not_null<QWidget*> target);
[[nodiscard]] ChosenReaction lookupChosen(const ReactionId &id) const;
[[nodiscard]] bool overCurrentButton(QPoint position) const;
void removeStaleButtons();
void paintButton(
QPainter &p,
const PaintContext &context,
not_null<Button*> button);
void paintButton(
QPainter &p,
const PaintContext &context,
not_null<Button*> button,
int frame,
float64 scale);
void paintInnerGradients(
QPainter &p,
const QColor &background,
not_null<Button*> button,
int scroll,
float64 expandRatio);
void clearAppearAnimations();
[[nodiscard]] QMargins innerMargins() const;
[[nodiscard]] QRect buttonInner() const;
[[nodiscard]] QRect buttonInner(not_null<Button*> button) const;
[[nodiscard]] ClickHandlerPtr computeButtonLink(QPoint position) const;
[[nodiscard]] ClickHandlerPtr resolveButtonLink(
const ReactionId &id) const;
void updateCurrentButton() const;
QSize _outer;
QRect _inner;
Strip _strip;
Ui::RoundAreaWithShadow _cachedRound;
QImage _expandedBuffer;
QColor _gradientBackground;
QImage _topGradient;
QImage _bottomGradient;
QColor _gradient;
rpl::event_stream<ChosenReaction> _chosen;
rpl::event_stream<FullMsgId> _expandChosen;
mutable base::flat_map<ReactionId, ClickHandlerPtr> _links;
mutable ClickHandlerPtr _expandLink;
rpl::variable<int> _uniqueLimit = 0;
bool _showingAll = false;
bool _tagsStrip = false;
std::optional<ButtonParameters> _scheduledParameters;
base::Timer _buttonShowTimer;
const Fn<void(QRect)> _buttonUpdate;
std::unique_ptr<Button> _button;
std::vector<std::unique_ptr<Button>> _buttonHiding;
FullMsgId _buttonContext;
mutable base::flat_map<ReactionId, ClickHandlerPtr> _reactionsLinks;
Fn<Fn<void()>(ReactionId)> _createChooseCallback;
base::flat_map<FullMsgId, QRect> _activeEffectAreas;
Ui::ReactionPaintInfo _currentReactionInfo;
base::flat_map<FullMsgId, Ui::ReactionPaintInfo> _collectedEffects;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<ReactionId> _faveRequests;
rpl::lifetime _lifetime;
};
void SetupManagerList(
not_null<Manager*> manager,
rpl::producer<HistoryItem*> items);
} // namespace HistoryView

View File

@@ -0,0 +1,509 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/reactions/history_view_reactions_list.h"
#include "history/view/reactions/history_view_reactions_tabs.h"
#include "boxes/peer_list_box.h"
#include "boxes/peers/prepare_short_info_box.h"
#include "window/window_session_controller.h"
#include "history/history_item.h"
#include "history/history.h"
#include "api/api_who_reacted.h"
#include "ui/controls/who_reacted_context_action.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/painter.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_message_reaction_id.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_peer.h"
#include "lang/lang_keys.h"
namespace HistoryView::Reactions {
namespace {
constexpr auto kPerPageFirst = 20;
constexpr auto kPerPage = 100;
using ::Data::ReactionId;
class Row final : public PeerListRow {
public:
Row(
uint64 id,
not_null<PeerData*> peer,
const Ui::Text::CustomEmojiFactory &factory,
QStringView reactionEntityData,
Fn<void(Row*)> repaint,
Fn<bool()> paused);
QSize rightActionSize() const override;
QMargins rightActionMargins() const override;
bool rightActionDisabled() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
private:
std::unique_ptr<Ui::Text::CustomEmoji> _custom;
Fn<bool()> _paused;
};
class Controller final : public PeerListController {
public:
Controller(
not_null<Window::SessionNavigation*> window,
FullMsgId itemId,
const ReactionId &selected,
rpl::producer<ReactionId> switches,
std::shared_ptr<Api::WhoReadList> whoReadIds);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
std::unique_ptr<PeerListRow> createRestoredRow(
not_null<PeerData*> peer) override;
std::unique_ptr<PeerListState> saveState() const override;
void restoreState(std::unique_ptr<PeerListState> state) override;
private:
using AllEntry = std::pair<not_null<PeerData*>, Data::ReactionId>;
struct SavedState : SavedStateBase {
ReactionId shownReaction;
base::flat_map<std::pair<PeerId, ReactionId>, uint64> idsMap;
uint64 idsCounter = 0;
std::vector<AllEntry> all;
QString allOffset;
std::vector<not_null<PeerData*>> filtered;
QString filteredOffset;
bool wasLoading = false;
};
void fillWhoRead();
void loadMore(const ReactionId &reaction);
bool appendRow(not_null<PeerData*> peer, ReactionId reaction);
std::unique_ptr<PeerListRow> createRow(
not_null<PeerData*> peer,
ReactionId reaction) const;
void showReaction(const ReactionId &reaction);
[[nodiscard]] uint64 id(
not_null<PeerData*> peer,
const ReactionId &reaction) const;
const not_null<Window::SessionNavigation*> _window;
const not_null<PeerData*> _peer;
const FullMsgId _itemId;
const Ui::Text::CustomEmojiFactory _factory;
const std::shared_ptr<Api::WhoReadList> _whoReadIds;
const std::vector<not_null<PeerData*>> _whoRead;
MTP::Sender _api;
ReactionId _shownReaction;
mutable base::flat_map<std::pair<PeerId, ReactionId>, uint64> _idsMap;
mutable uint64 _idsCounter = 0;
std::vector<AllEntry> _all;
QString _allOffset;
std::vector<not_null<PeerData*>> _filtered;
QString _filteredOffset;
mtpRequestId _loadRequestId = 0;
};
[[nodiscard]] std::vector<not_null<PeerData*>> ResolveWhoRead(
not_null<Window::SessionNavigation*> window,
const std::shared_ptr<Api::WhoReadList> &whoReadIds) {
if (!whoReadIds || whoReadIds->list.empty()) {
return {};
}
auto result = std::vector<not_null<PeerData*>>();
auto &owner = window->session().data();
for (const auto &peerWithDate : whoReadIds->list) {
if (const auto peer = owner.peerLoaded(peerWithDate.peer)) {
result.push_back(peer);
}
}
return result;
}
Row::Row(
uint64 id,
not_null<PeerData*> peer,
const Ui::Text::CustomEmojiFactory &factory,
QStringView reactionEntityData,
Fn<void(Row*)> repaint,
Fn<bool()> paused)
: PeerListRow(peer, id)
, _custom(reactionEntityData.isEmpty()
? nullptr
: factory(reactionEntityData, { .repaint = [=] { repaint(this); } }))
, _paused(std::move(paused)) {
}
QSize Row::rightActionSize() const {
const auto size = Ui::Emoji::GetSizeNormal() / style::DevicePixelRatio();
return _custom ? QSize(size, size) : QSize();
}
QMargins Row::rightActionMargins() const {
if (!_custom) {
return QMargins();
}
const auto size = Ui::Emoji::GetSizeNormal() / style::DevicePixelRatio();
return QMargins(
size / 2,
(st::defaultPeerList.item.height - size) / 2,
(size * 3) / 2,
0);
}
bool Row::rightActionDisabled() const {
return true;
}
void Row::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
if (!_custom) {
return;
}
const auto size = Ui::Emoji::GetSizeNormal() / style::DevicePixelRatio();
const auto skip = (size - Ui::Text::AdjustCustomEmojiSize(size)) / 2;
_custom->paint(p, {
.textColor = st::windowFg->c,
.now = crl::now(),
.position = { x + skip, y + skip },
.paused = _paused(),
});
}
Controller::Controller(
not_null<Window::SessionNavigation*> window,
FullMsgId itemId,
const ReactionId &selected,
rpl::producer<ReactionId> switches,
std::shared_ptr<Api::WhoReadList> whoReadIds)
: _window(window)
, _peer(window->session().data().peer(itemId.peer))
, _itemId(itemId)
, _factory(Data::ReactedMenuFactory(&window->session()))
, _whoReadIds(whoReadIds)
, _whoRead(ResolveWhoRead(window, _whoReadIds))
, _api(&window->session().mtp())
, _shownReaction(selected) {
std::move(
switches
) | rpl::filter([=](const ReactionId &reaction) {
return (_shownReaction != reaction);
}) | rpl::on_next([=](const ReactionId &reaction) {
showReaction(reaction);
}, lifetime());
}
Main::Session &Controller::session() const {
return _window->session();
}
void Controller::prepare() {
if (_shownReaction.emoji() == u"read"_q) {
fillWhoRead();
setDescriptionText(QString());
} else {
setDescriptionText(tr::lng_contacts_loading(tr::now));
}
delegate()->peerListRefreshRows();
loadMore(_shownReaction);
}
void Controller::showReaction(const ReactionId &reaction) {
if (_shownReaction == reaction) {
return;
}
_api.request(base::take(_loadRequestId)).cancel();
while (const auto count = delegate()->peerListFullRowsCount()) {
delegate()->peerListRemoveRow(delegate()->peerListRowAt(count - 1));
}
_shownReaction = reaction;
if (_shownReaction.emoji() == u"read"_q) {
fillWhoRead();
} else if (_shownReaction.empty()) {
_filtered.clear();
for (const auto &[peer, reaction] : _all) {
appendRow(peer, reaction);
}
} else {
_filtered = _all | ranges::views::filter([&](const AllEntry &entry) {
return (entry.second == reaction);
}) | ranges::views::transform(
&AllEntry::first
) | ranges::to_vector;
for (const auto peer : _filtered) {
appendRow(peer, _shownReaction);
}
_filteredOffset = QString();
}
loadMore(_shownReaction);
setDescriptionText(delegate()->peerListFullRowsCount()
? QString()
: tr::lng_contacts_loading(tr::now));
delegate()->peerListRefreshRows();
}
uint64 Controller::id(
not_null<PeerData*> peer,
const ReactionId &reaction) const {
const auto key = std::pair{ peer->id, reaction };
const auto i = _idsMap.find(key);
return (i != end(_idsMap)
? i
: _idsMap.emplace(key, ++_idsCounter).first)->second;
}
void Controller::fillWhoRead() {
for (const auto &peer : _whoRead) {
appendRow(peer, ReactionId());
}
}
void Controller::loadMoreRows() {
const auto &offset = _shownReaction.empty()
? _allOffset
: _filteredOffset;
if (_loadRequestId || offset.isEmpty()) {
return;
}
loadMore(_shownReaction);
}
std::unique_ptr<PeerListRow> Controller::createRestoredRow(
not_null<PeerData*> peer) {
if (_shownReaction.emoji() == u"read"_q) {
return createRow(peer, Data::ReactionId());
} else if (_shownReaction.empty()) {
const auto i = ranges::find(_all, peer, &AllEntry::first);
const auto reaction = (i != end(_all)) ? i->second : _shownReaction;
return createRow(peer, reaction);
}
return createRow(peer, _shownReaction);
}
std::unique_ptr<PeerListState> Controller::saveState() const {
auto result = PeerListController::saveState();
auto my = std::make_unique<SavedState>();
my->shownReaction = _shownReaction;
my->idsMap = _idsMap;
my->idsCounter = _idsCounter;
my->all = _all;
my->allOffset = _allOffset;
my->filtered = _filtered;
my->filteredOffset = _filteredOffset;
my->wasLoading = (_loadRequestId != 0);
result->controllerState = std::move(my);
return result;
}
void Controller::restoreState(std::unique_ptr<PeerListState> state) {
auto typeErasedState = state
? state->controllerState.get()
: nullptr;
if (const auto my = dynamic_cast<SavedState*>(typeErasedState)) {
if (const auto requestId = base::take(_loadRequestId)) {
_api.request(requestId).cancel();
}
_shownReaction = my->shownReaction;
_idsMap = std::move(my->idsMap);
_idsCounter = my->idsCounter;
_all = std::move(my->all);
_allOffset = std::move(my->allOffset);
_filtered = std::move(my->filtered);
_filteredOffset = std::move(my->filteredOffset);
if (my->wasLoading) {
loadMoreRows();
}
PeerListController::restoreState(std::move(state));
if (delegate()->peerListFullRowsCount()) {
setDescriptionText(QString());
delegate()->peerListRefreshRows();
}
}
}
void Controller::loadMore(const ReactionId &reaction) {
if (reaction.emoji() == u"read"_q) {
loadMore(ReactionId());
return;
} else if (reaction.empty() && _allOffset.isEmpty() && !_all.empty()) {
return;
}
_api.request(_loadRequestId).cancel();
const auto &offset = reaction.empty()
? _allOffset
: _filteredOffset;
using Flag = MTPmessages_GetMessageReactionsList::Flag;
const auto flags = Flag(0)
| (offset.isEmpty() ? Flag(0) : Flag::f_offset)
| (reaction.empty() ? Flag(0) : Flag::f_reaction);
_loadRequestId = _api.request(MTPmessages_GetMessageReactionsList(
MTP_flags(flags),
_peer->input(),
MTP_int(_itemId.msg),
Data::ReactionToMTP(reaction),
MTP_string(offset),
MTP_int(offset.isEmpty() ? kPerPageFirst : kPerPage)
)).done([=](const MTPmessages_MessageReactionsList &result) {
_loadRequestId = 0;
const auto filtered = !reaction.empty();
const auto shown = (reaction == _shownReaction);
result.match([&](const MTPDmessages_messageReactionsList &data) {
const auto sessionData = &session().data();
sessionData->processUsers(data.vusers());
sessionData->processChats(data.vchats());
(filtered ? _filteredOffset : _allOffset)
= data.vnext_offset().value_or_empty();
for (const auto &reaction : data.vreactions().v) {
reaction.match([&](const MTPDmessagePeerReaction &data) {
const auto peer = sessionData->peerLoaded(
peerFromMTP(data.vpeer_id()));
const auto reaction = Data::ReactionFromMTP(
data.vreaction());
if (peer && (!shown || appendRow(peer, reaction))) {
if (filtered) {
_filtered.emplace_back(peer);
} else {
_all.emplace_back(peer, reaction);
}
}
});
}
});
if (shown) {
setDescriptionText(QString());
delegate()->peerListRefreshRows();
}
}).send();
}
void Controller::rowClicked(not_null<PeerListRow*> row) {
const auto window = _window;
const auto peer = row->peer();
crl::on_main(window, [=] {
window->showPeerInfo(peer);
});
}
bool Controller::appendRow(not_null<PeerData*> peer, ReactionId reaction) {
if (delegate()->peerListFindRow(id(peer, reaction))) {
return false;
}
delegate()->peerListAppendRow(createRow(peer, reaction));
return true;
}
std::unique_ptr<PeerListRow> Controller::createRow(
not_null<PeerData*> peer,
ReactionId reaction) const {
return std::make_unique<Row>(
id(peer, reaction),
peer,
_factory,
Data::ReactionEntityData(reaction),
[=](Row *row) { delegate()->peerListUpdateRow(row); },
[=] { return _window->parentController()->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer); });
}
} // namespace
Data::ReactionId DefaultSelectedTab(
not_null<HistoryItem*> item,
std::shared_ptr<Api::WhoReadList> whoReadIds) {
return DefaultSelectedTab(item, {}, std::move(whoReadIds));
}
Data::ReactionId DefaultSelectedTab(
not_null<HistoryItem*> item,
Data::ReactionId selected,
std::shared_ptr<Api::WhoReadList> whoReadIds) {
const auto proj = &Data::MessageReaction::id;
if (!ranges::contains(item->reactions(), selected, proj)) {
selected = {};
}
return (selected.empty() && whoReadIds && !whoReadIds->list.empty())
? Data::ReactionId{ u"read"_q }
: selected;
}
not_null<Tabs*> CreateReactionsTabs(
not_null<QWidget*> parent,
not_null<Window::SessionNavigation*> window,
FullMsgId itemId,
Data::ReactionId selected,
std::shared_ptr<Api::WhoReadList> whoReadIds) {
const auto item = window->session().data().message(itemId);
auto map = item
? item->reactions()
: std::vector<Data::MessageReaction>();
if (whoReadIds && !whoReadIds->list.empty()) {
map.push_back({
.id = Data::ReactionId{ u"read"_q },
.count = int(whoReadIds->list.size()),
});
}
return CreateTabs(
parent,
Data::ReactedMenuFactory(&window->session()),
[=] { return window->parentController()->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer); },
map,
selected,
whoReadIds ? whoReadIds->type : Ui::WhoReadType::Reacted);
}
PreparedFullList FullListController(
not_null<Window::SessionNavigation*> window,
FullMsgId itemId,
Data::ReactionId selected,
std::shared_ptr<Api::WhoReadList> whoReadIds) {
Expects(IsServerMsgId(itemId.msg));
const auto tab = std::make_shared<
rpl::event_stream<Data::ReactionId>>();
return {
.controller = std::make_unique<Controller>(
window,
itemId,
selected,
tab->events(),
whoReadIds),
.switchTab = [=](Data::ReactionId id) { tab->fire_copy(id); },
};
}
} // namespace HistoryView::Reactions

View File

@@ -0,0 +1,61 @@
/*
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 "base/object_ptr.h"
class HistoryItem;
class PeerListController;
namespace Data {
struct ReactionId;
} // namespace Data
namespace Api {
struct WhoReadList;
} // namespace Api
namespace Window {
class SessionController;
class SessionNavigation;
} // namespace Window
namespace Ui {
class BoxContent;
} // namespace Ui
namespace HistoryView::Reactions {
[[nodiscard]] Data::ReactionId DefaultSelectedTab(
not_null<HistoryItem*> item,
std::shared_ptr<Api::WhoReadList> whoReadIds);
[[nodiscard]] Data::ReactionId DefaultSelectedTab(
not_null<HistoryItem*> item,
Data::ReactionId selected,
std::shared_ptr<Api::WhoReadList> whoReadIds = nullptr);
struct Tabs;
[[nodiscard]] not_null<Tabs*> CreateReactionsTabs(
not_null<QWidget*> parent,
not_null<Window::SessionNavigation*> window,
FullMsgId itemId,
Data::ReactionId selected,
std::shared_ptr<Api::WhoReadList> whoReadIds);
struct PreparedFullList {
std::unique_ptr<PeerListController> controller;
Fn<void(Data::ReactionId)> switchTab;
};
[[nodiscard]] PreparedFullList FullListController(
not_null<Window::SessionNavigation*> window,
FullMsgId itemId,
Data::ReactionId selected,
std::shared_ptr<Api::WhoReadList> whoReadIds = nullptr);
} // namespace HistoryView::Reactions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,292 @@
/*
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 "base/expected.h"
#include "base/unique_qptr.h"
#include "data/data_message_reactions.h"
#include "history/view/reactions/history_view_reactions_strip.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/round_area_with_shadow.h"
#include "ui/rp_widget.h"
namespace Data {
struct Reaction;
struct ReactionId;
} // namespace Data
namespace ChatHelpers {
class Show;
class TabbedPanel;
class EmojiListWidget;
class StickersListWidget;
class StickersListFooter;
enum class EmojiListMode;
} // namespace ChatHelpers
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class PopupMenu;
class ScrollArea;
class PlainShadow;
class FlatLabel;
} // namespace Ui
namespace HistoryView::Reactions {
class UnifiedFactoryOwner final {
public:
using RecentFactory = Fn<std::unique_ptr<Ui::Text::CustomEmoji>(
DocumentId,
Fn<void()>)>;
UnifiedFactoryOwner(
not_null<Main::Session*> session,
const std::vector<Data::Reaction> &reactions,
Strip *strip = nullptr);
[[nodiscard]] const std::vector<DocumentId> &unifiedIdsList() const {
return _unifiedIdsList;
}
[[nodiscard]] Data::ReactionId lookupReactionId(
DocumentId unifiedId) const;
[[nodiscard]] RecentFactory factory();
private:
const not_null<Main::Session*> _session;
Strip *_strip = nullptr;
std::vector<DocumentId> _unifiedIdsList;
base::flat_map<DocumentId, Data::ReactionId> _defaultReactionIds;
base::flat_map<DocumentId, int> _defaultReactionInStripMap;
QPoint _defaultReactionShift;
QPoint _stripPaintOneShift;
};
class Selector final : public Ui::RpWidget {
public:
Selector(
not_null<QWidget*> parent,
const style::EmojiPan &st,
std::shared_ptr<ChatHelpers::Show> show,
const Data::PossibleItemReactionsRef &reactions,
TextWithEntities about,
Fn<void(bool fast)> close,
IconFactory iconFactory = nullptr,
Fn<bool()> paused = nullptr,
bool child = false);
#if 0 // not ready
Selector(
not_null<QWidget*> parent,
const style::EmojiPan &st,
std::shared_ptr<ChatHelpers::Show> show,
ChatHelpers::EmojiListMode mode,
std::vector<DocumentId> recent,
Fn<void(bool fast)> close,
bool child = false);
#endif
~Selector();
[[nodiscard]] bool useTransparency() const;
int countWidth(int desiredWidth, int maxWidth);
[[nodiscard]] int effectPreviewHeight() const;
[[nodiscard]] QMargins marginsForShadow() const;
[[nodiscard]] int extendTopForCategories() const;
[[nodiscard]] int extendTopForCategoriesAndAbout(int width) const;
[[nodiscard]] int opaqueExtendTopAbout(int width) const;
[[nodiscard]] int minimalHeight(int fullWidth) const;
[[nodiscard]] int countAppearedWidth(float64 progress) const;
void setSpecialExpandTopSkip(int skip);
void setBubbleUp(bool bubbleUp);
void initGeometry(int innerTop);
void beforeDestroy();
void setOpaqueHeightExpand(int expand, Fn<void(int)> apply);
[[nodiscard]] rpl::producer<ChosenReaction> chosen() const {
return _chosen.events();
}
[[nodiscard]] rpl::producer<> willExpand() const {
return _willExpand.events();
}
[[nodiscard]] rpl::producer<> escapes() const;
void updateShowState(
float64 progress,
float64 opacity,
bool appearing,
bool toggling);
private:
static constexpr int kFramesCount = 32;
struct ExpandingRects {
QRect categories;
QRect list;
float64 radius = 0.;
float64 expanding = 0.;
int finalBottom = 0;
int frame = 0;
QRect outer;
};
Selector(
not_null<QWidget*> parent,
const style::EmojiPan &st,
std::shared_ptr<ChatHelpers::Show> show,
const Data::PossibleItemReactionsRef &reactions,
ChatHelpers::EmojiListMode mode,
std::vector<DocumentId> recent,
TextWithEntities about,
IconFactory iconFactory,
Fn<bool()> paused,
Fn<void(bool fast)> close,
bool child);
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void paintAppearing(QPainter &p);
void paintCollapsed(QPainter &p);
void paintExpanding(Painter &p, float64 progress);
void paintExpandingBg(QPainter &p, const ExpandingRects &rects);
void paintFadingExpandIcon(QPainter &p, float64 progress);
void paintExpanded(QPainter &p);
void paintNonTransparentExpandRect(QPainter &p, const QRect &) const;
void paintBubble(QPainter &p, int innerWidth);
void paintBackgroundToBuffer();
ExpandingRects updateExpandingRects(float64 progress);
[[nodiscard]] int recentCount() const;
[[nodiscard]] int countSkipLeft() const;
[[nodiscard]] int lookupSelectedIndex(QPoint position) const;
void setSelected(int index);
void expand();
void cacheExpandIcon();
void createList();
void finishExpand();
ChosenReaction lookupChosen(const Data::ReactionId &id) const;
void preloadAllRecentsAnimations();
[[nodiscard]] int skipYBubbleUpShift() const;
const style::EmojiPan &_st;
const std::shared_ptr<ChatHelpers::Show> _show;
const Data::PossibleItemReactions _reactions;
const std::vector<DocumentId> _recent;
const ChatHelpers::EmojiListMode _listMode;
const Fn<bool()> _paused;
Fn<void()> _jumpedToPremium;
Ui::RoundAreaWithShadow _cachedRound;
std::unique_ptr<Strip> _strip;
std::unique_ptr<Ui::FlatLabel> _about;
mutable int _aboutExtend = 0;
rpl::event_stream<ChosenReaction> _chosen;
rpl::event_stream<> _willExpand;
rpl::event_stream<> _escapes;
Ui::ScrollArea *_scroll = nullptr;
ChatHelpers::EmojiListWidget *_list = nullptr;
ChatHelpers::StickersListWidget *_stickers = nullptr;
ChatHelpers::StickersListFooter *_footer = nullptr;
std::unique_ptr<UnifiedFactoryOwner> _unifiedFactoryOwner;
Ui::PlainShadow *_shadow = nullptr;
rpl::variable<int> _shadowTop = 0;
rpl::variable<int> _shadowSkip = 0;
bool _showEmptySearch = false;
QImage _paintBuffer;
Ui::Animations::Simple _expanding;
float64 _appearProgress = 0.;
float64 _appearOpacity = 0.;
QRect _inner;
QRect _outer;
QRect _outerWithBubble;
QImage _expandIconCache;
QImage _aboutCache;
QMargins _padding;
int _specialExpandTopSkip = 0;
int _collapsedTopSkip = 0;
int _topAddOnExpand = 0;
int _opaqueHeightExpand = 0;
Fn<void(int)> _opaqueApplyHeightExpand;
const int _size = 0;
int _recentRows = 0;
int _columns = 0;
int _skipx = 0;
int _skipy = 0;
int _pressed = -1;
bool _useTransparency = false;
bool _appearing = false;
bool _toggling = false;
bool _expanded = false;
bool _expandScheduled = false;
bool _expandFinished = false;
bool _small = false;
bool _over = false;
bool _low = false;
bool _bubbleUp = false;
};
enum class AttachSelectorResult {
Skipped,
Failed,
Attached,
};
#if 0 // not ready
AttachSelectorResult MakeJustSelectorMenu(
not_null<Ui::PopupMenu*> menu,
not_null<Window::SessionController*> controller,
QPoint desiredPosition,
ChatHelpers::EmojiListMode mode,
std::vector<DocumentId> recent,
Fn<void(ChosenReaction)> chosen);
#endif
AttachSelectorResult AttachSelectorToMenu(
not_null<Ui::PopupMenu*> menu,
not_null<Window::SessionController*> controller,
QPoint desiredPosition,
not_null<HistoryItem*> item,
Fn<void(ChosenReaction)> chosen,
TextWithEntities about,
IconFactory iconFactory = nullptr);
[[nodiscard]] auto AttachSelectorToMenu(
not_null<Ui::PopupMenu*> menu,
QPoint desiredPosition,
const style::EmojiPan &st,
std::shared_ptr<ChatHelpers::Show> show,
const Data::PossibleItemReactionsRef &reactions,
TextWithEntities about,
IconFactory iconFactory = nullptr,
Fn<bool()> paused = nullptr
) -> base::expected<not_null<Selector*>, AttachSelectorResult>;
[[nodiscard]] TextWithEntities ItemReactionsAbout(
not_null<HistoryItem*> item);
} // namespace HistoryView::Reactions

View File

@@ -0,0 +1,576 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/reactions/history_view_reactions_strip.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "main/main_session.h"
#include "ui/effects/frame_generator.h"
#include "ui/animated_icon.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace HistoryView::Reactions {
namespace {
constexpr auto kSizeForDownscale = 96;
constexpr auto kEmojiCacheIndex = 0;
constexpr auto kHoverScaleDuration = crl::time(200);
constexpr auto kHoverScale = 1.24;
[[nodiscard]] int MainReactionSize() {
return style::ConvertScale(kSizeForDownscale);
}
[[nodiscard]] std::shared_ptr<Ui::AnimatedIcon> CreateIcon(
not_null<Data::DocumentMedia*> media,
int size) {
Expects(media->loaded());
return std::make_shared<Ui::AnimatedIcon>(Ui::AnimatedIconDescriptor{
.generator = DocumentIconFrameGenerator(media),
.sizeOverride = QSize(size, size),
.colorized = media->owner()->emojiUsesTextColor(),
});
}
} // namespace
Strip::Strip(
const style::EmojiPan &st,
QRect inner,
int size,
Fn<void()> update,
IconFactory iconFactory)
: _st(st)
, _iconFactory(iconFactory
? std::move(iconFactory)
: DefaultCachingIconFactory)
, _inner(inner)
, _finalSize(size)
, _update(std::move(update)) {
style::PaletteChanged(
) | rpl::on_next([=] {
invalidateMainReactionImage();
}, _lifetime);
}
void Strip::invalidateMainReactionImage() {
if (_mainReactionImage.isNull()
&& !ranges::contains(_validEmoji, true)) {
return;
}
const auto was = base::take(_mainReactionMedia);
_mainReactionImage = QImage();
ranges::fill(_validEmoji, false);
resolveMainReactionIcon();
}
void Strip::applyList(
const std::vector<not_null<const Data::Reaction*>> &list,
AddedButton button) {
if (_button == button
&& ranges::equal(
ranges::make_subrange(
begin(_icons),
(begin(_icons)
+ _icons.size()
- (_button == AddedButton::None ? 0 : 1))),
list,
ranges::equal_to(),
&ReactionIcons::id,
&Data::Reaction::id)) {
return;
}
const auto selected = _selectedIcon;
setSelected(-1);
_icons.clear();
for (const auto &reaction : list) {
_icons.push_back({
.id = reaction->id,
.appearAnimation = reaction->appearAnimation,
.selectAnimation = reaction->selectAnimation,
});
}
_button = button;
if (_button != AddedButton::None) {
_icons.push_back({ .added = _button });
}
setSelected((selected < _icons.size()) ? selected : -1);
resolveMainReactionIcon();
}
void Strip::paint(
QPainter &p,
QPoint position,
QPoint shift,
QRect clip,
float64 scale,
bool hiding) {
const auto skip = st::reactionAppearStartSkip;
const auto animationRect = clip.marginsRemoved({ 0, skip, 0, skip });
PainterHighQualityEnabler hq(p);
const auto countTarget = resolveCountTargetMethod(scale);
for (auto &icon : _icons) {
const auto target = countTarget(icon).translated(position);
position += shift;
if (target.intersects(clip)) {
paintOne(
p,
icon,
position - shift,
target,
!hiding && target.intersects(animationRect));
} else if (!hiding) {
clearStateForHidden(icon);
}
if (!hiding) {
clearStateForSelectFinished(icon);
}
}
}
auto Strip::resolveCountTargetMethod(float64 scale) const
-> Fn<QRectF(const ReactionIcons&)> {
const auto hoveredSize = int(base::SafeRound(_finalSize * kHoverScale));
const auto basicTargetForScale = [&](int size, float64 scale) {
const auto remove = size * (1. - scale) / 2.;
return QRectF(QRect(
_inner.x() + (_inner.width() - size) / 2,
_inner.y() + (_inner.height() - size) / 2,
size,
size
)).marginsRemoved({ remove, remove, remove, remove });
};
const auto basicTarget = basicTargetForScale(_finalSize, scale);
return [=](const ReactionIcons &icon) {
const auto selectScale = icon.selectedScale.value(
icon.selected ? kHoverScale : 1.);
if (selectScale == 1.) {
return basicTarget;
}
const auto finalScale = scale * selectScale;
return (finalScale <= 1.)
? basicTargetForScale(_finalSize, finalScale)
: basicTargetForScale(hoveredSize, finalScale / kHoverScale);
};
}
void Strip::paintOne(
QPainter &p,
ReactionIcons &icon,
QPoint position,
QRectF target,
bool allowAppearStart) {
if (icon.added == AddedButton::Expand) {
paintExpandIcon(p, position, target);
} else {
const auto paintFrame = [&](not_null<Ui::AnimatedIcon*> animation) {
const auto size = int(std::floor(target.width() + 0.01));
const auto &textColor = _st.textFg->c;
const auto frame = animation->frame(
textColor,
{ size, size },
_update);
p.drawImage(target, frame.image);
};
const auto appear = icon.appear.get();
if (appear && !icon.appearAnimated && allowAppearStart) {
icon.appearAnimated = true;
appear->animate(_update);
}
if (appear && appear->animating()) {
paintFrame(appear);
} else if (const auto select = icon.select.get()) {
paintFrame(select);
}
}
}
void Strip::paintOne(
QPainter &p,
int index,
QPoint position,
float64 scale) {
Expects(index >= 0 && index < _icons.size());
auto &icon = _icons[index];
const auto countTarget = resolveCountTargetMethod(scale);
const auto target = countTarget(icon).translated(position);
paintOne(p, icon, position, target, false);
}
bool Strip::inDefaultState(int index) const {
Expects(index >= 0 && index < _icons.size());
const auto &icon = _icons[index];
return !icon.selected
&& !icon.selectedScale.animating()
&& icon.select
&& !icon.select->animating()
&& (!icon.appear || !icon.appear->animating());
}
bool Strip::empty() const {
return _icons.empty();
}
int Strip::count() const {
return _icons.size();
}
bool Strip::onlyAddedButton() const {
return (_icons.size() == 1)
&& (_icons.front().added != AddedButton::None);
}
int Strip::fillChosenIconGetIndex(ChosenReaction &chosen) const {
const auto i = ranges::find(_icons, chosen.id, &ReactionIcons::id);
if (i == end(_icons)) {
return -1;
}
const auto &icon = *i;
if (const auto &appear = icon.appear; appear && appear->animating()) {
chosen.icon = appear->frame(_st.textFg->c);
} else if (const auto &select = icon.select; select && select->valid()) {
chosen.icon = select->frame(_st.textFg->c);
}
return (i - begin(_icons));
}
void Strip::paintExpandIcon(
QPainter &p,
QPoint position,
QRectF target) const {
const auto to = QRect(
_inner.x() + (_inner.width() - _finalSize) / 2,
_inner.y() + (_inner.height() - _finalSize) / 2,
_finalSize,
_finalSize
).translated(position);
const auto scale = target.width() / to.width();
if (scale != 1.) {
p.save();
p.translate(target.center());
p.scale(scale, scale);
p.translate(-target.center());
}
auto hq = PainterHighQualityEnabler(p);
((_finalSize == st::reactionCornerImage)
? _st.icons.stripExpandDropdown
: _st.icons.stripExpandPanel).paintInCenter(p, to);
if (scale != 1.) {
p.restore();
}
}
void Strip::setSelected(int index) const {
const auto set = [&](int index, bool selected) {
if (index < 0 || index >= _icons.size()) {
return;
}
auto &icon = _icons[index];
if (icon.selected == selected) {
return;
}
icon.selected = selected;
icon.selectedScale.start(
_update,
selected ? 1. : kHoverScale,
selected ? kHoverScale : 1.,
kHoverScaleDuration,
anim::sineInOut);
if (selected) {
const auto skipAnimation = icon.selectAnimated
|| !icon.appearAnimated
|| (icon.select && icon.select->animating())
|| (icon.appear && icon.appear->animating());
const auto select = skipAnimation ? nullptr : icon.select.get();
if (select && !icon.selectAnimated) {
icon.selectAnimated = true;
select->animate(_update);
}
}
};
if (_selectedIcon != index) {
set(_selectedIcon, false);
_selectedIcon = index;
}
set(index, true);
}
auto Strip::selected() const -> std::variant<AddedButton, ReactionId> {
if (_selectedIcon < 0 || _selectedIcon >= _icons.size()) {
return {};
}
const auto &icon = _icons[_selectedIcon];
if (icon.added != AddedButton::None) {
return icon.added;
}
return icon.id;
}
int Strip::computeOverSize() const {
return int(base::SafeRound(_finalSize * kHoverScale));
}
void Strip::clearAppearAnimations(bool mainAppeared) {
auto main = mainAppeared;
for (auto &icon : _icons) {
if (!main) {
if (icon.selected) {
setSelected(-1);
}
icon.selectedScale.stop();
if (const auto select = icon.select.get()) {
select->jumpToStart(nullptr);
}
icon.selectAnimated = false;
}
if (icon.appearAnimated != main) {
if (const auto appear = icon.appear.get()) {
appear->jumpToStart(nullptr);
}
icon.appearAnimated = main;
}
main = false;
}
}
void Strip::clearStateForHidden(ReactionIcons &icon) {
if (const auto appear = icon.appear.get()) {
appear->jumpToStart(nullptr);
}
if (icon.selected) {
setSelected(-1);
}
icon.appearAnimated = false;
icon.selectAnimated = false;
if (const auto select = icon.select.get()) {
select->jumpToStart(nullptr);
}
icon.selectedScale.stop();
}
void Strip::clearStateForSelectFinished(ReactionIcons &icon) {
if (icon.selectAnimated
&& !icon.select->animating()
&& !icon.selected) {
icon.selectAnimated = false;
}
}
bool Strip::checkIconLoaded(ReactionDocument &entry) const {
if (!entry.media) {
return true;
} else if (!entry.media->loaded()) {
return false;
}
const auto size = (entry.media == _mainReactionMedia)
? MainReactionSize()
: _finalSize;
entry.icon = _iconFactory(entry.media.get(), size);
entry.media = nullptr;
return true;
}
void Strip::loadIcons() {
const auto load = [&](not_null<DocumentData*> document) {
if (const auto i = _loadCache.find(document); i != end(_loadCache)) {
return i->second.icon;
}
auto &entry = _loadCache.emplace(document).first->second;
entry.media = document->createMediaView();
entry.media->checkStickerLarge();
if (!checkIconLoaded(entry) && !_loadCacheLifetime) {
document->session().downloaderTaskFinished(
) | rpl::on_next([=] {
checkIcons();
}, _loadCacheLifetime);
}
return entry.icon;
};
auto all = true;
for (auto &icon : _icons) {
if (icon.appearAnimation && !icon.appear) {
icon.appear = load(icon.appearAnimation);
if (!icon.appear) {
all = false;
}
}
if (icon.selectAnimation && !icon.select) {
icon.select = load(icon.selectAnimation);
if (!icon.select) {
all = false;
}
}
}
if (all && !_icons.empty() && _icons.front().selectAnimation) {
auto &data = _icons.front().selectAnimation->owner().reactions();
for (const auto &icon : _icons) {
data.preloadAnimationsFor(icon.id);
}
}
}
void Strip::checkIcons() {
auto all = true;
for (auto &[document, entry] : _loadCache) {
if (!checkIconLoaded(entry)) {
all = false;
}
}
if (all) {
_loadCacheLifetime.destroy();
loadIcons();
}
}
void Strip::resolveMainReactionIcon() {
if (_icons.empty() || onlyAddedButton()) {
_mainReactionMedia = nullptr;
_mainReactionLifetime.destroy();
return;
}
const auto main = _icons.front().selectAnimation;
Assert(main != nullptr);
_icons.front().appearAnimated = true;
if (_mainReactionMedia && _mainReactionMedia->owner() == main) {
if (!_mainReactionLifetime) {
loadIcons();
}
return;
}
_mainReactionMedia = main->createMediaView();
_mainReactionMedia->checkStickerLarge();
if (_mainReactionMedia->loaded()) {
setMainReactionIcon();
} else if (!_mainReactionLifetime) {
main->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _mainReactionMedia->loaded();
}) | rpl::take(1) | rpl::on_next([=] {
setMainReactionIcon();
}, _mainReactionLifetime);
}
}
void Strip::setMainReactionIcon() {
Expects(_mainReactionMedia->loaded());
_mainReactionLifetime.destroy();
ranges::fill(_validEmoji, false);
loadIcons();
Assert(_mainReactionMedia->loaded());
const auto i = _loadCache.find(_mainReactionMedia->owner());
if (i != end(_loadCache) && i->second.icon) {
const auto &icon = i->second.icon;
if (!icon->frameIndex() && icon->width() == MainReactionSize()) {
_mainReactionImage = i->second.icon->frame(_st.textFg->c);
return;
}
}
_mainReactionImage = QImage();
Assert(_mainReactionMedia->loaded());
_mainReactionIcon = DefaultIconFactory(
_mainReactionMedia.get(),
MainReactionSize());
}
bool Strip::onlyMainEmojiVisible() const {
if (_icons.empty()) {
return true;
}
const auto &icon = _icons.front();
if (icon.selected
|| icon.selectedScale.animating()
|| (icon.select && icon.select->animating())) {
return false;
}
icon.selectAnimated = false;
return true;
}
Ui::ImageSubrect Strip::validateEmoji(int frameIndex, float64 scale) {
const auto area = _inner.size();
const auto size = int(base::SafeRound(_finalSize * scale));
const auto result = Ui::ImageSubrect{
&_emojiParts,
Ui::RoundAreaWithShadow::FrameCacheRect(
frameIndex,
kEmojiCacheIndex,
area),
};
if (_validEmoji[frameIndex]) {
return result;
} else if (_emojiParts.isNull()) {
_emojiParts = Ui::RoundAreaWithShadow::PrepareFramesCache(area);
}
auto p = QPainter(result.image);
const auto ratio = style::DevicePixelRatio();
const auto position = result.rect.topLeft() / ratio;
p.setCompositionMode(QPainter::CompositionMode_Source);
p.fillRect(QRect(position, result.rect.size() / ratio), Qt::transparent);
if (_mainReactionImage.isNull()
&& _mainReactionIcon) {
_mainReactionImage = base::take(_mainReactionIcon)->frame(
_st.textFg->c);
}
if (!_mainReactionImage.isNull()) {
const auto target = QRect(
(_inner.width() - size) / 2,
(_inner.height() - size) / 2,
size,
size
).translated(position);
p.drawImage(target, _mainReactionImage.scaled(
target.size() * ratio,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation));
}
_validEmoji[frameIndex] = true;
return result;
}
IconFactory CachedIconFactory::createMethod() {
return [=](not_null<Data::DocumentMedia*> media, int size) {
const auto owned = media->owner()->createMediaView();
const auto i = _cache.find(owned);
return (i != end(_cache))
? i->second
: _cache.emplace(
owned,
DefaultIconFactory(media, size)).first->second;
};
}
std::shared_ptr<Ui::AnimatedIcon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,
int size) {
return CreateIcon(media, size);
}
std::shared_ptr<Ui::AnimatedIcon> DefaultCachingIconFactory(
not_null<Data::DocumentMedia*> media,
int size) {
auto &factory = media->owner()->session().cachedReactionIconFactory();
return factory.createMethod()(media, size);
}
} // namespace HistoryView::Reactions

View File

@@ -0,0 +1,180 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/effects/animations.h"
#include "ui/effects/round_area_with_shadow.h"
#include "data/data_message_reaction_id.h"
namespace style {
struct EmojiPan;
} // namespace style
class HistoryItem;
namespace Data {
struct Reaction;
class DocumentMedia;
} // namespace Data
namespace Ui {
class AnimatedIcon;
} // namespace Ui
namespace HistoryView::Reactions {
struct ChosenReaction {
FullMsgId context;
Data::ReactionId id;
QImage icon;
QRect localGeometry;
QRect globalGeometry;
explicit operator bool() const {
return context && !id.empty();
}
};
using IconFactory = Fn<std::shared_ptr<Ui::AnimatedIcon>(
not_null<Data::DocumentMedia*>,
int)>;
class Strip final {
public:
using ReactionId = Data::ReactionId;
Strip(
const style::EmojiPan &st,
QRect inner,
int size,
Fn<void()> update,
IconFactory iconFactory = nullptr);
enum class AddedButton : uchar {
None,
Expand,
};
void applyList(
const std::vector<not_null<const Data::Reaction*>> &list,
AddedButton button);
void paint(
QPainter &p,
QPoint position,
QPoint shift,
QRect clip,
float64 scale,
bool hiding);
void paintOne(QPainter &p, int index, QPoint position, float64 scale);
[[nodiscard]] bool inDefaultState(int index) const;
[[nodiscard]] bool empty() const;
[[nodiscard]] int count() const;
void setSelected(int index) const;
[[nodiscard]] std::variant<AddedButton, ReactionId> selected() const;
[[nodiscard]] int computeOverSize() const;
void clearAppearAnimations(bool mainAppeared = true);
int fillChosenIconGetIndex(ChosenReaction &chosen) const;
[[nodiscard]] bool onlyAddedButton() const;
[[nodiscard]] bool onlyMainEmojiVisible() const;
Ui::ImageSubrect validateEmoji(int frameIndex, float64 scale);
private:
static constexpr auto kFramesCount
= Ui::RoundAreaWithShadow::kFramesCount;
struct ReactionDocument {
std::shared_ptr<Data::DocumentMedia> media;
std::shared_ptr<Ui::AnimatedIcon> icon;
};
struct ReactionIcons {
ReactionId id;
DocumentData *appearAnimation = nullptr;
DocumentData *selectAnimation = nullptr;
std::shared_ptr<Ui::AnimatedIcon> appear;
std::shared_ptr<Ui::AnimatedIcon> select;
mutable Ui::Animations::Simple selectedScale;
AddedButton added = AddedButton::None;
bool appearAnimated = false;
mutable bool selected = false;
mutable bool selectAnimated = false;
};
void clearStateForHidden(ReactionIcons &icon);
void paintExpandIcon(QPainter &p, QPoint position, QRectF target) const;
void clearStateForSelectFinished(ReactionIcons &icon);
[[nodiscard]] bool checkIconLoaded(ReactionDocument &entry) const;
void loadIcons();
void checkIcons();
void paintOne(
QPainter &p,
ReactionIcons &icon,
QPoint position,
QRectF target,
bool allowAppearStart);
[[nodiscard]] Fn<QRectF(const ReactionIcons&)> resolveCountTargetMethod(
float64 scale) const;
void invalidateMainReactionImage();
void resolveMainReactionIcon();
void setMainReactionIcon();
const style::EmojiPan &_st;
const IconFactory _iconFactory;
const QRect _inner;
const int _finalSize = 0;
Fn<void()> _update;
std::vector<ReactionIcons> _icons;
AddedButton _button = AddedButton::None;
base::flat_map<not_null<DocumentData*>, ReactionDocument> _loadCache;
std::optional<ReactionIcons> _premiumIcon;
rpl::lifetime _loadCacheLifetime;
mutable int _selectedIcon = -1;
std::shared_ptr<Data::DocumentMedia> _mainReactionMedia;
std::shared_ptr<Ui::AnimatedIcon> _mainReactionIcon;
QImage _mainReactionImage;
rpl::lifetime _mainReactionLifetime;
QImage _emojiParts;
std::array<bool, kFramesCount> _validEmoji = { { false } };
rpl::lifetime _lifetime;
};
class CachedIconFactory final {
public:
CachedIconFactory() = default;
CachedIconFactory(const CachedIconFactory &other) = delete;
CachedIconFactory &operator=(const CachedIconFactory &other) = delete;
[[nodiscard]] IconFactory createMethod();
private:
base::flat_map<
std::shared_ptr<Data::DocumentMedia>,
std::shared_ptr<Ui::AnimatedIcon>> _cache;
};
[[nodiscard]] std::shared_ptr<Ui::AnimatedIcon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,
int size);
[[nodiscard]] std::shared_ptr<Ui::AnimatedIcon> DefaultCachingIconFactory(
not_null<Data::DocumentMedia*> media,
int size);
} // namespace HistoryView

View File

@@ -0,0 +1,214 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/reactions/history_view_reactions_tabs.h"
#include "data/data_message_reaction_id.h"
#include "lang/lang_tag.h"
#include "ui/abstract_button.h"
#include "ui/controls/who_reacted_context_action.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "styles/style_chat.h"
#include "styles/style_widgets.h"
namespace HistoryView::Reactions {
namespace {
using ::Data::ReactionId;
not_null<Ui::AbstractButton*> CreateTab(
not_null<QWidget*> parent,
const style::MultiSelect &st,
const Ui::Text::CustomEmojiFactory &factory,
Fn<bool()> paused,
const ReactionId &reaction,
Ui::WhoReadType whoReadType,
int count,
rpl::producer<bool> selected) {
struct State {
std::unique_ptr<Ui::Text::CustomEmoji> custom;
QImage cache;
bool selected = false;
};
const auto stm = &st.item;
const auto text = Lang::FormatCountDecimal(count);
const auto font = st::semiboldFont;
const auto textWidth = font->width(text);
const auto result = Ui::CreateChild<Ui::AbstractButton>(parent.get());
const auto width = stm->height
+ stm->padding.left()
+ textWidth
+ stm->padding.right();
result->resize(width, stm->height);
const auto state = result->lifetime().make_state<State>();
std::move(
selected
) | rpl::on_next([=](bool selected) {
state->selected = selected;
state->cache = QImage();
result->update();
}, result->lifetime());
state->custom = reaction.empty()
? nullptr
: factory(
Data::ReactionEntityData(reaction),
{ .repaint = [=] { result->update(); } });
result->paintRequest(
) | rpl::on_next([=] {
const auto factor = style::DevicePixelRatio();
const auto height = stm->height;
const auto skip = st::reactionsTabIconSkip;
const auto icon = QRect(skip, 0, height, height);
if (state->cache.isNull()) {
state->cache = QImage(
result->size() * factor,
QImage::Format_ARGB32_Premultiplied);
state->cache.setDevicePixelRatio(factor);
state->cache.fill(Qt::transparent);
auto p = QPainter(&state->cache);
const auto height = stm->height;
const auto radius = height / 2;
p.setPen(Qt::NoPen);
p.setBrush(state->selected ? stm->textActiveBg : stm->textBg);
{
PainterHighQualityEnabler hq(p);
p.drawRoundedRect(result->rect(), radius, radius);
}
const auto skip = st::reactionsTabIconSkip;
const auto icon = QRect(skip, 0, height, height);
if (!state->custom) {
using Type = Ui::WhoReadType;
(reaction.emoji().isEmpty()
? (state->selected
? st::reactionsTabAllSelected
: st::reactionsTabAll)
: (whoReadType == Type::Watched
|| whoReadType == Type::Listened)
? (state->selected
? st::reactionsTabPlayedSelected
: st::reactionsTabPlayed)
: (state->selected
? st::reactionsTabChecksSelected
: st::reactionsTabChecks)).paintInCenter(p, icon);
}
const auto textLeft = height + stm->padding.left();
p.setPen(state->selected ? stm->textActiveFg : stm->textFg);
p.setFont(font);
p.drawText(textLeft, stm->padding.top() + font->ascent, text);
}
auto p = QPainter(result);
p.drawImage(0, 0, state->cache);
if (const auto custom = state->custom.get()) {
using namespace Ui::Text;
const auto size = Ui::Emoji::GetSizeNormal() / factor;
const auto shift = (height - size) / 2;
const auto skip = (size - AdjustCustomEmojiSize(size)) / 2;
custom->paint(p, {
.textColor = (state->selected
? stm->textActiveFg
: stm->textFg)->c,
.now = crl::now(),
.position = { icon.x() + shift + skip, shift + skip },
});
}
}, result->lifetime());
return result;
}
} // namespace
not_null<Tabs*> CreateTabs(
not_null<QWidget*> parent,
Ui::Text::CustomEmojiFactory factory,
Fn<bool()> paused,
const std::vector<Data::MessageReaction> &items,
const Data::ReactionId &selected,
Ui::WhoReadType whoReadType) {
struct State {
rpl::variable<ReactionId> selected;
std::vector<not_null<Ui::AbstractButton*>> tabs;
};
const auto result = Ui::CreateChild<Tabs>(parent.get());
using Entry = std::pair<int, ReactionId>;
auto tabs = Ui::CreateChild<Ui::RpWidget>(parent.get());
const auto st = &st::reactionsTabs;
const auto state = tabs->lifetime().make_state<State>();
state->selected = selected;
const auto append = [&](const ReactionId &reaction, int count) {
using namespace rpl::mappers;
const auto tab = CreateTab(
tabs,
*st,
factory,
paused,
reaction,
whoReadType,
count,
state->selected.value() | rpl::map(_1 == reaction));
tab->setClickedCallback([=] {
state->selected = reaction;
});
state->tabs.push_back(tab);
};
auto sorted = std::vector<Entry>();
for (const auto &reaction : items) {
if (reaction.id.emoji() == u"read"_q) {
append(reaction.id, reaction.count);
} else {
sorted.emplace_back(reaction.count, reaction.id);
}
}
ranges::sort(sorted, std::greater<>(), &Entry::first);
const auto count = ranges::accumulate(
sorted,
0,
std::plus<>(),
&Entry::first);
append(ReactionId(), count);
for (const auto &[count, reaction] : sorted) {
append(reaction, count);
}
result->move = [=](int x, int y) {
tabs->moveToLeft(x, y);
};
result->resizeToWidth = [=](int width) {
const auto available = width
- st->padding.left()
- st->padding.right();
if (available <= 0) {
return;
}
auto left = available;
auto height = st->padding.top();
for (const auto &tab : state->tabs) {
if (left > 0 && available - left < tab->width()) {
left = 0;
height += tab->height() + st->itemSkip;
}
tab->move(
st->padding.left() + left,
height - tab->height() - st->itemSkip);
left += tab->width() + st->itemSkip;
}
tabs->resize(width, height - st->itemSkip + st->padding.bottom());
};
result->heightValue = [=] {
using namespace rpl::mappers;
return tabs->heightValue() | rpl::map(_1 - st::lineWidth);
};
result->changes = [=] {
return state->selected.changes();
};
return result;
}
} // namespace HistoryView::Reactions

View File

@@ -0,0 +1,38 @@
/*
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/text/text_custom_emoji.h" // Ui::Text::CustomEmojiFactory.
namespace Ui {
enum class WhoReadType;
} // namespace Ui
namespace Data {
struct ReactionId;
struct MessageReaction;
} // namespace Data
namespace HistoryView::Reactions {
struct Tabs {
Fn<void(int, int)> move;
Fn<void(int)> resizeToWidth;
Fn<rpl::producer<Data::ReactionId>()> changes;
Fn<rpl::producer<int>()> heightValue;
};
[[nodiscard]] not_null<Tabs*> CreateTabs(
not_null<QWidget*> parent,
Ui::Text::CustomEmojiFactory factory,
Fn<bool()> paused,
const std::vector<Data::MessageReaction> &items,
const Data::ReactionId &selected,
Ui::WhoReadType whoReadType);
} // namespace HistoryView::Reactions