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
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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ¶meters) {
|
||||
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
|
||||
@@ -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 ¶meters);
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user