Files
AyuGramDesktop/Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp
T
2025-11-15 11:59:18 +04:00

926 lines
26 KiB
C++

/*
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 "calls/group/calls_group_messages_ui.h"
#include "boxes/peers/prepare_short_info_box.h"
#include "boxes/premium_preview_box.h"
#include "calls/group/ui/calls_group_stars_coloring.h"
#include "calls/group/calls_group_messages.h"
#include "chat_helpers/compose/compose_show.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_message_reactions.h"
#include "data/data_message_reaction_id.h"
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/controls/emoji_button.h"
#include "ui/controls/send_button.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "ui/effects/reaction_fly_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/color_int_conversion.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/userpic_view.h"
#include "styles/style_calls.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_chat.h"
#include "styles/style_credits.h"
#include "styles/style_media_view.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QWindow>
namespace Calls::Group {
namespace {
constexpr auto kMessageBgOpacity = 0.8;
constexpr auto kColoredMessageBgOpacity = 0.6;
[[nodiscard]] int CountMessageRadius() {
const auto minHeight = st::groupCallMessagePadding.top()
+ st::messageTextStyle.font->height
+ st::groupCallMessagePadding.bottom();
return minHeight / 2;
}
[[nodiscard]] uint64 ColoringKey(const Ui::StarsColoring &value) {
return (uint64(uint32(value.bg1)) << 32) | uint64(uint32(value.bg2));
}
void ReceiveSomeMouseEvents(
not_null<Ui::ElasticScroll*> scroll,
Fn<bool(QPoint)> handleClick) {
class EventFilter final : public QObject {
public:
explicit EventFilter(
not_null<Ui::ElasticScroll*> scroll,
Fn<bool(QPoint)> handleClick)
: QObject(scroll)
, _handleClick(std::move(handleClick)) {
}
bool eventFilter(QObject *watched, QEvent *event) {
if (event->type() == QEvent::MouseButtonPress) {
return mousePressFilter(
watched,
static_cast<QMouseEvent*>(event));
} else if (event->type() == QEvent::Wheel) {
return wheelFilter(
watched,
static_cast<QWheelEvent*>(event));
}
return false;
}
bool mousePressFilter(
QObject *watched,
not_null<QMouseEvent*> event) {
Expects(parent()->isWidgetType());
const auto scroll = static_cast<Ui::ElasticScroll*>(parent());
if (watched != scroll->window()->windowHandle()) {
return false;
}
const auto global = event->globalPos();
const auto local = scroll->mapFromGlobal(global);
if (!scroll->rect().contains(local)) {
return false;
}
return _handleClick(local + QPoint(0, scroll->scrollTop()));
}
bool wheelFilter(QObject *watched, not_null<QWheelEvent*> event) {
Expects(parent()->isWidgetType());
const auto scroll = static_cast<Ui::ElasticScroll*>(parent());
if (watched != scroll->window()->windowHandle()
|| !scroll->scrollTopMax()) {
return false;
}
const auto global = event->globalPosition().toPoint();
const auto local = scroll->mapFromGlobal(global);
if (!scroll->rect().contains(local)) {
return false;
}
auto e = QWheelEvent(
event->position(),
event->globalPosition(),
event->pixelDelta(),
event->angleDelta(),
event->buttons(),
event->modifiers(),
event->phase(),
event->inverted(),
event->source());
e.setTimestamp(crl::now());
QGuiApplication::sendEvent(scroll, &e);
return true;
}
private:
Fn<bool(QPoint)> _handleClick;
};
scroll->setAttribute(Qt::WA_TransparentForMouseEvents);
qApp->installEventFilter(
new EventFilter(scroll, std::move(handleClick)));
}
} // namespace
struct MessagesUi::MessageView {
MsgId id = 0;
MsgId sendingId = 0;
PeerData *from = nullptr;
ClickHandlerPtr fromLink;
Ui::Animations::Simple toggleAnimation;
Ui::Animations::Simple sentAnimation;
Data::ReactionId reactionId;
std::unique_ptr<Ui::InfiniteRadialAnimation> sendingAnimation;
std::unique_ptr<Ui::ReactionFlyAnimation> reactionAnimation;
std::unique_ptr<Ui::RpWidget> reactionWidget;
QPoint reactionShift;
Ui::PeerUserpicView view;
Ui::Text::String text;
Ui::Text::String price;
int stars = 0;
int top = 0;
int width = 0;
int left = 0;
int height = 0;
int realHeight = 0;
bool removed = false;
bool sending = false;
bool failed = false;
};
MessagesUi::PayedBg::PayedBg(const Ui::StarsColoring &coloring)
: color(Ui::ColorFromSerialized(coloring.bg2))
, rounded(CountMessageRadius(), color.color()) {
}
MessagesUi::MessagesUi(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
MessagesMode mode,
rpl::producer<std::vector<Message>> messages,
rpl::producer<MessageIdUpdate> idUpdates,
rpl::producer<bool> shown)
: _parent(parent)
, _show(std::move(show))
, _mode(mode)
, _messageBg([] {
auto result = st::groupCallBg->c;
result.setAlphaF(kMessageBgOpacity);
return result;
})
, _messageBgRect(CountMessageRadius(), _messageBg.color())
, _fadeHeight(st::normalFont->height)
, _streamMode(_mode == MessagesMode::VideoStream) {
setupList(std::move(messages), std::move(shown));
handleIdUpdates(std::move(idUpdates));
}
MessagesUi::~MessagesUi() = default;
void MessagesUi::setupList(
rpl::producer<std::vector<Message>> messages,
rpl::producer<bool> shown) {
rpl::combine(
std::move(messages),
std::move(shown)
) | rpl::start_with_next([=](std::vector<Message> &&list, bool shown) {
if (!shown) {
list.clear();
}
auto from = begin(list);
auto till = end(list);
for (auto &entry : _views) {
if (!entry.removed) {
const auto id = entry.id;
const auto i = ranges::find(
from,
till,
id,
&Message::id);
if (i == till) {
toggleMessage(entry, false);
continue;
} else if (entry.failed != i->failed) {
setContentFailed(entry);
updateMessageSize(entry);
repaintMessage(entry.id);
} else if (entry.sending != (i->date == 0)) {
animateMessageSent(entry);
}
if (i == from) {
++from;
}
}
}
auto addedSendingToBottom = false;
for (auto i = from; i != till; ++i) {
if (!ranges::contains(_views, i->id, &MessageView::id)) {
if (i + 1 == till && !i->date) {
addedSendingToBottom = true;
}
appendMessage(*i);
}
}
if (addedSendingToBottom) {
const auto from = _scroll->scrollTop();
const auto till = _scroll->scrollTopMax();
_scrollToBottomAnimation.start([=] {
_scroll->scrollToY(_scrollToBottomAnimation.value(till));
}, from, till, st::slideDuration, anim::easeOutCirc);
}
}, _lifetime);
}
void MessagesUi::handleIdUpdates(rpl::producer<MessageIdUpdate> idUpdates) {
std::move(
idUpdates
) | rpl::start_with_next([=](MessageIdUpdate update) {
const auto i = ranges::find(
_views,
update.localId,
&MessageView::id);
if (i == end(_views)) {
return;
}
i->sendingId = update.localId;
i->id = update.realId;
if (_revealedSpoilerId == update.localId) {
_revealedSpoilerId = update.realId;
}
}, _lifetime);
}
void MessagesUi::animateMessageSent(MessageView &entry) {
const auto id = entry.id;
entry.sending = false;
entry.sentAnimation.start([=] {
repaintMessage(id);
}, 0., 1, st::slideDuration, anim::easeOutCirc);
repaintMessage(id);
}
void MessagesUi::updateMessageSize(MessageView &entry) {
const auto &padding = st::groupCallMessagePadding;
const auto hasUserpic = !entry.failed;
const auto userpicPadding = st::groupCallUserpicPadding;
const auto userpicSize = st::groupCallUserpic;
const auto leftSkip = hasUserpic
? (userpicPadding.left() + userpicSize + userpicPadding.right())
: padding.left();
const auto widthSkip = leftSkip + padding.right();
const auto inner = _width - widthSkip;
const auto size = Ui::Text::CountOptimalTextSize(
entry.text,
std::min(st::groupCallWidth / 2, inner),
inner);
const auto textHeight = size.height();
entry.width = size.width() + widthSkip;
entry.left = _streamMode ? 0 : (_width - entry.width) / 2;
updateReactionPosition(entry);
const auto contentHeight = padding.top() + textHeight + padding.bottom();
const auto userpicHeight = hasUserpic
? (userpicPadding.top() + userpicSize + userpicPadding.bottom())
: 0;
const auto skip = st::groupCallMessageSkip;
entry.realHeight = skip + std::max(contentHeight, userpicHeight);
}
bool MessagesUi::updateMessageHeight(MessageView &entry) {
const auto height = entry.toggleAnimation.animating()
? anim::interpolate(
0,
entry.realHeight,
entry.toggleAnimation.value(entry.removed ? 0. : 1.))
: entry.realHeight;
if (entry.height == height) {
return false;
}
entry.height = height;
return true;
}
void MessagesUi::setContentFailed(MessageView &entry) {
entry.failed = true;
entry.text = Ui::Text::String(
st::messageTextStyle,
TextWithEntities().append(
QString::fromUtf8("\xe2\x9d\x97\xef\xb8\x8f")
).append(' ').append(
Ui::Text::Italic(u"Failed to send the message."_q)),
kMarkupTextOptions,
st::groupCallWidth / 4);
entry.price = Ui::Text::String();
}
void MessagesUi::setContent(
MessageView &entry,
const TextWithEntities &text,
int stars) {
if (stars) {
entry.price = Ui::Text::String(
st::whoReadDateStyle,
Ui::Text::IconEmoji(
&st::starIconEmojiSmall
).append(Lang::FormatCountDecimal(stars)),
kMarkupTextOptions);
} else {
entry.price = Ui::Text::String();
}
entry.text = Ui::Text::String(
st::messageTextStyle,
text,
kMarkupTextOptions,
st::groupCallWidth / 4,
Core::TextContext({
.session = &_show->session(),
.repaint = [this, id = entry.id] { repaintMessage(id); },
}));
if (!entry.price.isEmpty()) {
entry.text.updateSkipBlock(
entry.price.maxWidth(),
st::normalFont->height);
}
entry.text.setLink(1, entry.fromLink);
if (entry.text.hasSpoilers()) {
const auto id = entry.id;
const auto guard = base::make_weak(_messages);
entry.text.setSpoilerLinkFilter([=](const ClickContext &context) {
if (context.button != Qt::LeftButton || !guard) {
return false;
}
const auto i = ranges::find(
_views,
_revealedSpoilerId,
&MessageView::id);
if (i != end(_views) && _revealedSpoilerId != id) {
i->text.setSpoilerRevealed(false, anim::type::normal);
}
_revealedSpoilerId = id;
return true;
});
}
}
void MessagesUi::toggleMessage(MessageView &entry, bool shown) {
const auto id = entry.id;
entry.removed = !shown;
entry.toggleAnimation.start(
[=] { repaintMessage(id); },
shown ? 0. : 1.,
shown ? 1. : 0.,
st::slideWrapDuration,
shown ? anim::easeOutCirc : anim::easeInCirc);
repaintMessage(id);
}
void MessagesUi::repaintMessage(MsgId id) {
auto i = ranges::find(_views, id, &MessageView::id);
if (i == end(_views) && id < 0) {
i = ranges::find(_views, id, &MessageView::sendingId);
}
if (i == end(_views)) {
return;
} else if (i->removed && !i->toggleAnimation.animating()) {
const auto top = i->top;
i = _views.erase(i);
recountHeights(i, top);
return;
}
if (!i->sending && !i->sentAnimation.animating()) {
i->sendingAnimation = nullptr;
}
if (i->toggleAnimation.animating() || i->height != i->realHeight) {
if (updateMessageHeight(*i)) {
recountHeights(i, i->top);
return;
}
}
_messages->update(0, i->top, _messages->width(), i->height);
}
void MessagesUi::recountHeights(
std::vector<MessageView>::iterator i,
int top) {
auto from = top;
for (auto e = end(_views); i != e; ++i) {
i->top = top;
top += i->height;
updateReactionPosition(*i);
}
if (_views.empty()) {
delete base::take(_messages);
_scroll = nullptr;
} else {
updateGeometries();
_messages->update(0, from, _messages->width(), top - from);
}
}
void MessagesUi::appendMessage(const Message &data) {
const auto top = _views.empty()
? 0
: (_views.back().top + _views.back().height);
if (!_scroll) {
setupMessagesWidget();
}
auto &entry = _views.emplace_back();
const auto id = entry.id = data.id;
entry.stars = data.stars;
const auto repaint = [=] {
repaintMessage(id);
};
const auto peer = entry.from = data.peer;
entry.fromLink = std::make_shared<LambdaClickHandler>([=] {
_show->show(
PrepareShortInfoBox(peer, _show, &st::storiesShortInfoBox));
});
entry.sending = !data.date;
if (data.failed) {
setContentFailed(entry);
} else {
auto text = Ui::Text::Link(Ui::Text::Bold(data.peer->shortName()), 1)
.append(' ').append(data.text);
setContent(entry, text, data.stars);
}
entry.top = top;
updateMessageSize(entry);
if (entry.sending) {
using namespace Ui;
const auto &st = st::defaultInfiniteRadialAnimation;
entry.sendingAnimation = std::make_unique<InfiniteRadialAnimation>(
repaint,
st);
entry.sendingAnimation->start(0);
}
entry.height = 0;
toggleMessage(entry, true);
checkReactionContent(entry, data.text);
}
void MessagesUi::checkReactionContent(
MessageView &entry,
const TextWithEntities &text) {
auto outLength = 0;
using Type = Data::Reactions::Type;
const auto reactions = &_show->session().data().reactions();
const auto set = [&](Data::ReactionId id) {
reactions->preloadAnimationsFor(id);
entry.reactionId = std::move(id);
};
if (text.entities.size() == 1
&& text.entities.front().type() == EntityType::CustomEmoji
&& text.entities.front().offset() == 0
&& text.entities.front().length() == text.text.size()) {
set({ text.entities.front().data().toULongLong() });
} else if (const auto emoji = Ui::Emoji::Find(text.text, &outLength)) {
if (outLength < text.text.size()) {
return;
}
const auto &all = reactions->list(Type::All);
for (const auto &reaction : all) {
if (reaction.id.custom()) {
continue;
}
const auto &text = reaction.id.emoji();
if (Ui::Emoji::Find(text) != emoji) {
continue;
}
set(reaction.id);
break;
}
}
}
void MessagesUi::startReactionAnimation(MessageView &entry) {
entry.reactionWidget = std::make_unique<Ui::RpWidget>(_parent);
const auto raw = entry.reactionWidget.get();
raw->show();
raw->setAttribute(Qt::WA_TransparentForMouseEvents);
if (!_effectsLifetime) {
rpl::combine(
_scroll->scrollTopValue(),
_scroll->RpWidget::positionValue()
) | rpl::start_with_next([=](int yshift, QPoint point) {
_reactionBasePosition = point - QPoint(0, yshift);
for (auto &view : _views) {
updateReactionPosition(view);
}
}, _effectsLifetime);
}
entry.reactionAnimation = std::make_unique<Ui::ReactionFlyAnimation>(
&_show->session().data().reactions(),
Ui::ReactionFlyAnimationArgs{
.id = entry.reactionId,
.effectOnly = true,
},
[=] { raw->update(); },
st::reactionInlineImage);
updateReactionPosition(entry);
const auto effectSize = st::reactionInlineImage * 2;
const auto animation = entry.reactionAnimation.get();
raw->resize(effectSize, effectSize);
raw->paintRequest() | rpl::start_with_next([=] {
if (animation->finished()) {
crl::on_main(raw, [=] {
removeReaction(raw);
});
return;
}
auto p = QPainter(raw);
const auto size = raw->width();
const auto skip = (size - st::reactionInlineImage) / 2;
const auto target = QRect(
QPoint(skip, skip),
QSize(st::reactionInlineImage, st::reactionInlineImage));
animation->paintGetArea(
p,
QPoint(),
target,
st::radialFg->c,
QRect(),
crl::now());
}, raw->lifetime());
}
void MessagesUi::removeReaction(not_null<Ui::RpWidget*> widget) {
const auto i = ranges::find_if(_views, [&](const MessageView &entry) {
return entry.reactionWidget.get() == widget;
});
if (i != end(_views)) {
i->reactionId = {};
i->reactionWidget = nullptr;
i->reactionAnimation = nullptr;
};
}
void MessagesUi::updateReactionPosition(MessageView &entry) {
if (const auto widget = entry.reactionWidget.get()) {
if (entry.failed) {
widget->resize(0, 0);
return;
}
const auto padding = st::groupCallMessagePadding;
const auto userpicSize = st::groupCallUserpic;
const auto userpicPadding = st::groupCallUserpicPadding;
const auto esize = st::emojiSize;
const auto eleft = entry.text.maxWidth() - st::emojiPadding - esize;
const auto etop = (st::normalFont->height - esize) / 2;
const auto effectSize = st::reactionInlineImage * 2;
entry.reactionShift = QPoint(entry.left, entry.top)
+ QPoint(
userpicPadding.left() + userpicSize + userpicPadding.right(),
padding.top())
+ QPoint(eleft + (esize / 2), etop + (esize / 2))
- QPoint(effectSize / 2, effectSize / 2);
widget->move(_reactionBasePosition + entry.reactionShift);
}
}
void MessagesUi::updateTopFade() {
const auto topFadeShown = (_scroll->scrollTop() > 0);
if (_topFadeShown != topFadeShown) {
_topFadeShown = topFadeShown;
//const auto from = topFadeShown ? 0. : 1.;
//const auto till = topFadeShown ? 1. : 0.;
//_topFadeAnimation.start([=] {
_messages->update(
0,
_scroll->scrollTop(),
_messages->width(),
_fadeHeight);
//}, from, till, st::slideWrapDuration);
}
}
void MessagesUi::updateBottomFade() {
const auto max = _scroll->scrollTopMax();
const auto bottomFadeShown = (_scroll->scrollTop() < max);
if (_bottomFadeShown != bottomFadeShown) {
_bottomFadeShown = bottomFadeShown;
//const auto from = bottomFadeShown ? 0. : 1.;
//const auto till = bottomFadeShown ? 1. : 0.;
//_bottomFadeAnimation.start([=] {
_messages->update(
0,
_scroll->scrollTop() + _scroll->height() - _fadeHeight,
_messages->width(),
_fadeHeight);
//}, from, till, st::slideWrapDuration);
}
}
void MessagesUi::setupMessagesWidget() {
_scroll = std::make_unique<Ui::ElasticScroll>(
_parent,
st::groupCallMessagesScroll);
const auto scroll = _scroll.get();
_messages = scroll->setOwnedWidget(object_ptr<Ui::RpWidget>(scroll));
rpl::combine(
scroll->scrollTopValue(),
scroll->heightValue(),
_messages->heightValue()
) | rpl::start_with_next([=] {
updateTopFade();
updateBottomFade();
}, scroll->lifetime());
ReceiveSomeMouseEvents(scroll, [=](QPoint point) {
for (const auto &entry : _views) {
if (entry.failed || entry.top + entry.height <= point.y()) {
continue;
} else if (entry.top >= point.y()
|| entry.left >= point.x()
|| entry.left + entry.width <= point.x()) {
return false;
}
const auto padding = st::groupCallMessagePadding;
const auto userpicSize = st::groupCallUserpic;
const auto userpicPadding = st::groupCallUserpicPadding;
const auto leftSkip = userpicPadding.left()
+ userpicSize
+ userpicPadding.right();
const auto userpic = QRect(
entry.left + userpicPadding.left(),
entry.top + userpicPadding.top(),
userpicSize,
userpicSize);
const auto link = userpic.contains(point)
? entry.fromLink
: entry.text.getState(point - QPoint(
entry.left + leftSkip,
entry.top + padding.top()
), entry.width - leftSkip - padding.right()).link;
if (link) {
ActivateClickHandler(_messages, link, Qt::LeftButton);
}
return true;
}
return false;
});
_messages->paintRequest() | rpl::start_with_next([=](QRect clip) {
const auto start = scroll->scrollTop();
const auto end = start + scroll->height();
const auto ratio = style::DevicePixelRatio();
if ((_canvas.width() < scroll->width() * ratio)
|| (_canvas.height() < scroll->height() * ratio)) {
_canvas = QImage(
scroll->size() * ratio,
QImage::Format_ARGB32_Premultiplied);
_canvas.setDevicePixelRatio(ratio);
}
auto p = Painter(&_canvas);
p.setCompositionMode(QPainter::CompositionMode_Clear);
p.fillRect(QRect(QPoint(), scroll->size()), QColor(0, 0, 0, 0));
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
const auto now = crl::now();
const auto skip = st::groupCallMessageSkip;
const auto padding = st::groupCallMessagePadding;
p.translate(0, -start);
for (auto &entry : _views) {
if (entry.height <= skip || entry.top + entry.height <= start) {
continue;
} else if (entry.top >= end) {
break;
}
const auto x = entry.left;
const auto y = entry.top;
const auto use = entry.realHeight - skip;
const auto width = entry.width;
p.setBrush(st::radialBg);
p.setPen(Qt::NoPen);
const auto scaled = (entry.height < entry.realHeight);
if (scaled) {
const auto used = entry.height - skip;
const auto mx = scaled ? (x + (width / 2)) : 0;
const auto my = scaled ? (y + (used / 2)) : 0;
const auto scale = used / float64(use);
p.save();
p.translate(mx, my);
p.scale(scale, scale);
p.setOpacity(scale);
p.translate(-mx, -my);
}
if (!_streamMode) {
_messageBgRect.paint(p, { x, y, width, use });
} else if (entry.stars) {
const auto coloring = Ui::StarsColoringForCount(entry.stars);
auto &bg = _bgs[ColoringKey(coloring)];
if (!bg) {
bg = std::make_unique<PayedBg>(coloring);
}
p.setOpacity(kColoredMessageBgOpacity);
bg->rounded.paint(p, { x, y, width, use });
p.setOpacity(1.);
}
auto leftSkip = padding.left();
const auto priceSkip = padding.right() / 2;
const auto hasUserpic = !entry.failed;
if (hasUserpic) {
const auto userpicSize = st::groupCallUserpic;
const auto userpicPadding = st::groupCallUserpicPadding;
const auto position = QPoint(
x + userpicPadding.left(),
y + userpicPadding.top());
const auto rect = QRect(
position,
QSize(userpicSize, userpicSize));
entry.from->paintUserpic(p, entry.view, {
.position = position,
.size = userpicSize,
.shape = Ui::PeerUserpicShape::Circle,
});
if (const auto animation = entry.sendingAnimation.get()) {
auto hq = PainterHighQualityEnabler(p);
auto pen = st::groupCallBg->p;
const auto shift = userpicPadding.left();
pen.setWidthF(shift);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
const auto state = animation->computeState();
const auto sent = entry.sending
? 0.
: entry.sentAnimation.value(1.);
p.setOpacity(state.shown * (1. - sent));
p.drawArc(
rect.marginsRemoved({ shift, shift, shift, shift }),
state.arcFrom,
state.arcLength);
p.setOpacity(1.);
}
leftSkip = userpicPadding.left()
+ userpicSize
+ userpicPadding.right();
}
p.setPen(st::radialFg);
entry.text.draw(p, {
.position = {
x + leftSkip,
y + padding.top(),
},
.availableWidth = entry.width - leftSkip - padding.right(),
.palette = &st::groupCallMessagePalette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = now,
.paused = !_messages->window()->isActiveWindow(),
});
if (!entry.price.isEmpty()) {
entry.price.draw(p, {
.position = {
x + entry.width - entry.price.maxWidth() - priceSkip,
y + use - st::normalFont->height,
},
.availableWidth = entry.price.maxWidth(),
});
}
if (!scaled && entry.reactionId && !entry.reactionAnimation) {
startReactionAnimation(entry);
}
p.restore();
}
p.translate(0, start);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p.setPen(Qt::NoPen);
const auto topFade = _topFadeAnimation.value(
_topFadeShown ? 1. : 0.);
if (topFade) {
auto gradientTop = QLinearGradient(0, 0, 0, _fadeHeight);
gradientTop.setStops({
{ 0., QColor(255, 255, 255, 0) },
{ 1., QColor(255, 255, 255, 255) },
});
p.setOpacity(topFade);
p.setBrush(gradientTop);
p.drawRect(0, 0, scroll->width(), _fadeHeight);
p.setOpacity(1.);
}
const auto bottomFade = _bottomFadeAnimation.value(
_bottomFadeShown ? 1. : 0.);
if (bottomFade) {
const auto till = scroll->height();
const auto from = till - _fadeHeight;
auto gradientBottom = QLinearGradient(0, from, 0, till);
gradientBottom.setStops({
{ 0., QColor(255, 255, 255, 255) },
{ 1., QColor(255, 255, 255, 0) },
});
p.setBrush(gradientBottom);
p.drawRect(0, from, scroll->width(), _fadeHeight);
}
QPainter(_messages).drawImage(
QRect(QPoint(0, start), scroll->size()),
_canvas,
QRect(QPoint(), scroll->size() * ratio));
}, _lifetime);
scroll->show();
applyWidth();
}
void MessagesUi::applyWidth() {
if (!_scroll || _width < st::groupCallWidth * 2 / 3) {
return;
}
auto top = 0;
for (auto &entry : _views) {
entry.top = top;
updateMessageSize(entry);
updateMessageHeight(entry);
top += entry.height;
}
updateGeometries();
}
void MessagesUi::updateGeometries() {
const auto scrollBottom = (_scroll->scrollTop() + _scroll->height());
const auto atBottom = (scrollBottom >= _messages->height());
const auto height = _views.empty()
? 0
: (_views.back().top + _views.back().height);
_messages->setGeometry(0, 0, _width, height);
const auto min = std::min(height, _availableHeight);
_scroll->setGeometry(_left, _bottom - min, _width, min);
if (atBottom) {
_scroll->scrollToY(std::max(height - _scroll->height(), 0));
}
}
void MessagesUi::move(int left, int bottom, int width, int availableHeight) {
if (_left != left
|| _bottom != bottom
|| _width != width
|| _availableHeight != availableHeight) {
_left = left;
_bottom = bottom;
_width = width;
_availableHeight = availableHeight;
applyWidth();
}
}
void MessagesUi::raise() {
if (_scroll) {
_scroll->raise();
}
for (const auto &view : _views) {
if (const auto widget = view.reactionWidget.get()) {
widget->raise();
}
}
}
rpl::lifetime &MessagesUi::lifetime() {
return _lifetime;
}
} // namespace Calls::Group