Files
tdesktop/Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp
2025-12-15 21:40:54 +04:00

1914 lines
52 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 "base/unixtime.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_app_config.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.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/layers/generic_box.h"
#include "ui/text/custom_emoji_text_badge.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/widgets/popup_menu.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_info_levels.h"
#include "styles/style_media_view.h"
#include "styles/style_menu_icons.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QWindow>
namespace Calls::Group {
namespace {
constexpr auto kMessageBgOpacity = 0.8;
constexpr auto kDarkOverOpacity = 0.25;
constexpr auto kColoredMessageBgOpacity = 0.65;
constexpr auto kAdminBadgeTextOpacity = 0.6;
[[nodiscard]] int CountMessageRadius() {
const auto minHeight = st::groupCallMessagePadding.top()
+ st::messageTextStyle.font->height
+ st::groupCallMessagePadding.bottom();
return minHeight / 2;
}
[[nodiscard]] int CountPriceRadius() {
const auto height = st::groupCallPricePadding.top()
+ st::normalFont->height
+ st::groupCallPricePadding.bottom();
return height / 2;
}
[[nodiscard]] int CountPinnedRadius() {
const auto height = st::groupCallUserpicPadding.top()
+ st::groupCallPinnedUserpic
+ st::groupCallUserpicPadding.bottom();
return height / 2;
}
[[nodiscard]] uint64 ColoringKey(const Ui::StarsColoring &value) {
return uint64(uint32(value.bgLight))
| (uint64(uint32(value.bgDark)) << 32);
}
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)));
}
[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeMessageDateAction(
not_null<Ui::PopupMenu*> menu,
TimeId value) {
const auto parent = menu->menu();
const auto parsed = base::unixtime::parse(value);
const auto date = parsed.date();
const auto time = QLocale().toString(
parsed.time(),
QLocale::ShortFormat);
const auto today = QDateTime::currentDateTime().date();
const auto text = (date == today)
? tr::lng_context_sent_today(tr::now, lt_time, time)
: (date.addDays(1) == today)
? tr::lng_context_sent_yesterday(tr::now, lt_time, time)
: tr::lng_context_sent_date(
tr::now,
lt_date,
langDayOfMonthFull(date),
lt_time,
time);
const auto action = Ui::Menu::CreateAction(parent, text, [] {});
action->setDisabled(true);
return base::make_unique_q<Ui::Menu::Action>(
menu->menu(),
st::storiesCommentSentAt,
action,
nullptr,
nullptr);
}
void ShowDeleteMessageConfirmation(
std::shared_ptr<Ui::Show> show,
MsgId id,
not_null<PeerData*> from,
bool canModerate,
Fn<void(MessageDeleteRequest)> callback) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
struct State {
rpl::variable<bool> report;
rpl::variable<bool> all;
rpl::variable<bool> ban;
};
const auto state = box->lifetime().make_state<State>();
const auto confirmed = [=](Fn<void()> close) {
callback(MessageDeleteRequest{
.id = id,
.deleteAllFrom = state->all.current() ? from.get() : nullptr,
.ban = state->ban.current() ? from.get() : nullptr,
.reportSpam = state->report.current(),
});
close();
};
Ui::ConfirmBox(box, {
.text = tr::lng_selected_delete_sure_this(),
.confirmed = confirmed,
.confirmText = tr::lng_box_delete(),
.labelStyle = &st::groupCallBoxLabel,
});
if (canModerate) {
const auto check = [&](rpl::producer<QString> text) {
const auto add = st::groupCallCheckbox.margin;
const auto added = QMargins(0, add.top(), 0, add.bottom());
const auto margin = st::boxRowPadding + added;
return box->addRow(object_ptr<Ui::Checkbox>(
box,
std::move(text),
false,
st::groupCallCheckbox
), margin)->checkedValue();
};
state->report = check(tr::lng_report_spam());
state->all = check(tr::lng_delete_all_from_user(
lt_user,
rpl::single(from->shortName()))),
state->ban = check(tr::lng_ban_user());
}
}));
}
[[nodiscard]] QImage CrownMask(int place) {
const auto &icon = st::paidReactCrownSmall;
const auto size = icon.size();
const auto ratio = style::DevicePixelRatio();
const auto full = size * ratio;
auto result = QImage(full, QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(ratio);
auto p = QPainter(&result);
icon.paint(p, 0, 0, size.width(), QColor(255, 255, 255));
const auto top = st::paidReactCrownSmallTop;
p.setCompositionMode(QPainter::CompositionMode_Source);
p.setPen(Qt::transparent);
p.setFont(st::levelStyle.font);
p.drawText(
QRect(0, top, icon.width(), icon.height()),
QString::number(place),
style::al_top);
p.end();
return result;
}
} // namespace
struct MessagesUi::MessageView {
MsgId id = 0;
MsgId sendingId = 0;
not_null<PeerData*> from;
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;
TextWithEntities original;
Ui::PeerUserpicView view;
Ui::Text::String name;
Ui::Text::String text;
Ui::Text::String price;
TimeId date = 0;
int stars = 0;
int place = 0;
int top = 0;
int width = 0;
int left = 0;
int height = 0;
int realHeight = 0;
int nameWidth = 0;
int textLeft = 0;
int textTop = 0;
bool removed = false;
bool sending = false;
bool failed = false;
bool simple = false;
bool admin = false;
bool mine = false;
};
struct MessagesUi::PinnedView {
MsgId id = 0;
not_null<PeerData*> from;
Ui::Animations::Simple toggleAnimation;
Ui::PeerUserpicView view;
Ui::Text::String text;
crl::time duration = 0;
crl::time end = 0;
int stars = 0;
int place = 0;
int top = 0;
int width = 0;
int left = 0;
int height = 0;
int realWidth = 0;
bool removed = false;
bool requiresSmooth = false;
};
MessagesUi::PayedBg::PayedBg(const Ui::StarsColoring &coloring)
: light(Ui::ColorFromSerialized(coloring.bgLight))
, dark(Ui::ColorFromSerialized(coloring.bgDark))
, pinnedLight(CountPinnedRadius(), light.color())
, pinnedDark(CountPinnedRadius(), dark.color())
, messageLight(CountMessageRadius(), light.color())
, priceDark(CountPriceRadius(), dark.color())
, badgeDark(st::roundRadiusLarge, dark.color()) {
}
MessagesUi::MessagesUi(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
MessagesMode mode,
rpl::producer<std::vector<Message>> messages,
rpl::producer<std::vector<not_null<PeerData*>>> topDonorsValue,
rpl::producer<MessageIdUpdate> idUpdates,
rpl::producer<bool> canManageValue,
rpl::producer<bool> shown)
: _parent(parent)
, _show(std::move(show))
, _mode(mode)
, _canManage(std::move(canManageValue))
, _messageBg([] {
auto result = st::groupCallBg->c;
result.setAlphaF(kMessageBgOpacity);
return result;
})
, _messageBgRect(CountMessageRadius(), _messageBg.color())
, _crownHelper(Core::TextContext({ .session = &_show->session() }))
, _topDonors(std::move(topDonorsValue))
, _fadeHeight(st::normalFont->height * 2)
, _fadeWidth(st::normalFont->height * 2)
, _streamMode(_mode == MessagesMode::VideoStream) {
setupBadges();
setupList(std::move(messages), std::move(shown));
handleIdUpdates(std::move(idUpdates));
}
MessagesUi::~MessagesUi() = default;
void MessagesUi::setupBadges() {
auto helper = Ui::Text::CustomEmojiHelper();
const auto liveText = helper.paletteDependent(
Ui::Text::CustomEmojiTextBadge(
tr::lng_video_stream_live(tr::now),
st::groupCallMessageBadge,
st::groupCallMessageBadgeMargin));
_liveBadge.setMarkedText(
st::messageTextStyle,
liveText,
kMarkupTextOptions,
helper.context());
_adminBadge.setText(st::messageTextStyle, tr::lng_admin_badge(tr::now));
_topDonors.value(
) | rpl::on_next([=] {
for (auto &entry : _views) {
const auto place = donorPlace(entry.from);
if (entry.place != place) {
entry.place = place;
if (!entry.failed) {
setContent(entry);
}
}
}
for (auto &entry : _pinnedViews) {
const auto place = donorPlace(entry.from);
if (entry.place != place) {
entry.place = place;
setContent(entry);
}
}
applyGeometry();
}, _lifetime);
}
void MessagesUi::setupList(
rpl::producer<std::vector<Message>> messages,
rpl::producer<bool> shown) {
rpl::combine(
std::move(messages),
std::move(shown)
) | rpl::on_next([=](std::vector<Message> &&list, bool shown) {
if (shown) {
_hidden = std::nullopt;
} else {
_hidden = base::take(list);
}
showList(list);
}, _lifetime);
}
void MessagesUi::showList(const std::vector<Message> &list) {
const auto now = base::unixtime::now();
auto from = begin(list);
auto till = end(list);
for (auto &entry : _views) {
if (entry.removed) {
continue;
}
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);
}
entry.date = i->date;
if (i == from) {
appendPinned(*i, now);
++from;
}
}
auto addedSendingToBottom = false;
for (auto i = from; i != till; ++i) {
const auto j = ranges::find(_views, i->id, &MessageView::id);
if (j != end(_views)) {
if (!j->removed) {
continue;
}
if (j->failed != i->failed) {
setContentFailed(*j);
updateMessageSize(*j);
repaintMessage(j->id);
} else if (j->sending != (i->date == 0)) {
animateMessageSent(*j);
}
j->date = i->date;
toggleMessage(*j, true);
} else {
if (i + 1 == till && !i->date) {
addedSendingToBottom = true;
}
appendMessage(*i);
appendPinned(*i, now);
}
}
if (addedSendingToBottom) {
const auto from = _scroll->scrollTop();
const auto till = _scroll->scrollTopMax();
if (from >= till) {
return;
}
_scrollToAnimation.stop();
_scrollToAnimation.start([=] {
_scroll->scrollToY(_scroll->scrollTopMax()
- _scrollToAnimation.value(0));
}, till - from, 0, st::slideDuration, anim::easeOutCirc);
}
}
void MessagesUi::handleIdUpdates(rpl::producer<MessageIdUpdate> idUpdates) {
std::move(
idUpdates
) | rpl::on_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 &pricePadding = st::groupCallPricePadding;
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 price = entry.simple
? (pricePadding.left() + pricePadding.right() + entry.price.maxWidth())
: 0;
const auto space = st::normalFont->spacew;
const auto nameWidth = entry.name.isEmpty() ? 0 : entry.name.maxWidth();
const auto nameLineWidth = nameWidth
? (nameWidth
+ space
+ _liveBadge.maxWidth()
+ space
+ _adminBadge.maxWidth())
: 0;
const auto nameHeight = entry.name.isEmpty()
? 0
: st::messageTextStyle.font->height;
const auto textHeight = size.height();
entry.width = widthSkip
+ std::max(size.width() + price, std::min(nameLineWidth, inner));
entry.left = _streamMode ? 0 : (_width - entry.width) / 2;
entry.textLeft = leftSkip;
entry.textTop = padding.top() + nameHeight;
entry.nameWidth = std::min(
nameWidth,
(entry.width
- widthSkip
- space
- _liveBadge.maxWidth()
- space
- _adminBadge.maxWidth()));
updateReactionPosition(entry);
const auto contentHeight = entry.textTop + 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::updatePinnedSize(PinnedView &entry) {
const auto &padding = st::groupCallPinnedPadding;
const auto userpicPadding = st::groupCallUserpicPadding;
const auto userpicSize = st::groupCallPinnedUserpic;
const auto leftSkip = userpicPadding.left()
+ userpicSize
+ userpicPadding.right();
const auto inner = std::min(
entry.text.maxWidth(),
st::groupCallPinnedMaxWidth);
entry.height = userpicPadding.top()
+ userpicSize
+ userpicPadding.bottom();
entry.top = 0;
const auto skip = st::groupCallMessageSkip;
entry.realWidth = skip + leftSkip + inner + padding.right();
const auto ratio = style::DevicePixelRatio();
entry.requiresSmooth = (entry.realWidth * ratio * 1000 > entry.duration);
}
bool MessagesUi::updatePinnedWidth(PinnedView &entry) {
const auto width = entry.toggleAnimation.animating()
? anim::interpolate(
0,
entry.realWidth,
entry.toggleAnimation.value(entry.removed ? 0. : 1.))
: entry.realWidth;
if (entry.width == width) {
return false;
}
entry.width = width;
return true;
}
void MessagesUi::setContentFailed(MessageView &entry) {
entry.failed = true;
entry.name = Ui::Text::String();
entry.text = Ui::Text::String(
st::messageTextStyle,
TextWithEntities().append(
QString::fromUtf8("\xe2\x9d\x97\xef\xb8\x8f")
).append(' ').append(
tr::italic(u"Failed to send the message."_q)),
kMarkupTextOptions,
st::groupCallWidth / 8);
entry.price = Ui::Text::String();
}
void MessagesUi::setContent(MessageView &entry) {
entry.simple = !entry.admin && entry.original.empty() && entry.stars > 0;
const auto name = nameText(entry.from, entry.place);
entry.name = entry.admin
? Ui::Text::String(
st::messageTextStyle,
name,
kMarkupTextOptions,
Ui::kQFixedMax,
_crownHelper.context())
: Ui::Text::String();
if (const auto stars = entry.stars) {
entry.price = Ui::Text::String(
entry.simple ? st::messageTextStyle : st::whoReadDateStyle,
Ui::Text::IconEmoji(
&st::starIconEmojiSmall
).append(Lang::FormatCountDecimal(stars)),
kMarkupTextOptions);
} else {
entry.price = Ui::Text::String();
}
auto composed = entry.admin
? entry.original
: tr::link(name, 1).append(' ').append(entry.original);
if (!entry.admin) {
composed.text.replace(QChar('\n'), QChar(' '));
}
entry.text = Ui::Text::String(
st::messageTextStyle,
composed,
kMarkupTextOptions,
st::groupCallWidth / 8,
_crownHelper.context([this, id = entry.id] { repaintMessage(id); }));
if (!entry.simple && !entry.price.isEmpty()) {
entry.text.updateSkipBlock(
entry.price.maxWidth(),
st::normalFont->height);
}
if (!entry.simple && !entry.admin) {
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::setContent(PinnedView &entry) {
const auto text = nameText(entry.from, entry.place);
entry.text.setMarkedText(
st::messageTextStyle,
text,
kMarkupTextOptions,
_crownHelper.context());
}
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() && id == _delayedHighlightId) {
highlightMessage(base::take(_delayedHighlightId));
}
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()) {
_scrollToAnimation.stop();
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();
}
const auto id = data.id;
const auto peer = data.peer;
auto &entry = _views.emplace_back(MessageView{
.id = id,
.from = peer,
.original = data.text,
.date = data.date,
.stars = data.stars,
.place = donorPlace(peer),
.top = top,
.sending = !data.date,
.admin = data.admin && _streamMode,
.mine = data.mine,
});
const auto repaint = [=] {
repaintMessage(id);
};
entry.fromLink = std::make_shared<LambdaClickHandler>([=] {
_show->show(
PrepareShortInfoBox(peer, _show, &st::storiesShortInfoBox));
});
if (data.failed) {
setContentFailed(entry);
} else {
setContent(entry);
}
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::togglePinned(PinnedView &entry, bool shown) {
const auto id = entry.id;
entry.removed = !shown;
entry.toggleAnimation.start(
[=] { repaintPinned(id); },
shown ? 0. : 1.,
shown ? 1. : 0.,
st::slideWrapDuration,
shown ? anim::easeOutCirc : anim::easeInCirc);
repaintPinned(id);
}
void MessagesUi::repaintPinned(MsgId id) {
const auto i = ranges::find(_pinnedViews, id, &PinnedView::id);
if (i == end(_pinnedViews)) {
return;
} else if (i->removed && !i->toggleAnimation.animating()) {
const auto left = i->left;
recountWidths(_pinnedViews.erase(i), left);
return;
}
if (i->toggleAnimation.animating() || i->width != i->realWidth) {
const auto was = i->width;
if (updatePinnedWidth(*i)) {
if (i->width > was) {
const auto larger = countPinnedScrollSkip(*i);
if (larger > _pinnedScrollSkip) {
setPinnedScrollSkip(larger);
}
} else {
applyGeometryToPinned();
}
recountWidths(i, i->left);
return;
}
}
_pinned->update(i->left, 0, i->width, _pinned->height());
}
void MessagesUi::recountWidths(
std::vector<PinnedView>::iterator i,
int left) {
auto from = left;
for (auto e = end(_pinnedViews); i != e; ++i) {
i->left = left;
left += i->width;
}
if (_pinnedViews.empty()) {
delete base::take(_pinned);
_pinnedScroll = nullptr;
} else {
updateGeometries();
_pinned->update(from, 0, left - from, _pinned->height());
}
}
void MessagesUi::appendPinned(const Message &data, TimeId now) {
if (!data.date
|| data.pinFinishDate <= data.date
|| data.pinFinishDate <= now
|| ranges::contains(
_pinnedViews,
data.id,
&PinnedView::id)) {
return;
}
const auto peer = data.peer;
const auto finishes = crl::now()
+ (data.pinFinishDate - now) * crl::time(1000);
const auto i = ranges::find(_pinnedViews, peer, &PinnedView::from);
if (i != end(_pinnedViews)) {
if (i->end > finishes) {
return;
}
const auto left = i->left;
recountWidths(_pinnedViews.erase(i), left);
}
if (!_pinnedScroll) {
setupPinnedWidget();
}
const auto j = ranges::lower_bound(
_pinnedViews,
data.stars,
ranges::greater(),
&PinnedView::stars);
const auto left = (j != end(_pinnedViews))
? j->left
: _pinnedViews.empty()
? 0
: (_pinnedViews.back().left + _pinnedViews.back().width);
auto &entry = *_pinnedViews.insert(j, PinnedView{
.id = data.id,
.from = peer,
.duration = (data.pinFinishDate - data.date) * crl::time(1000),
.end = finishes,
.stars = data.stars,
.place = donorPlace(peer),
.left = left,
});
setContent(entry);
updatePinnedSize(entry);
entry.width = 0;
togglePinned(entry, true);
}
int MessagesUi::donorPlace(not_null<PeerData*> peer) const {
const auto &donors = _topDonors.current();
const auto i = ranges::find(donors, peer);
if (i == end(donors)) {
return 0;
}
return static_cast<int>(std::distance(begin(donors), i)) + 1;
}
TextWithEntities MessagesUi::nameText(
not_null<PeerData*> peer,
int place) {
auto result = TextWithEntities();
if (place > 0) {
auto i = _crownEmojiDataCache.find(place);
if (i == _crownEmojiDataCache.end()) {
i = _crownEmojiDataCache.emplace(
place,
_crownHelper.imageData(Ui::Text::ImageEmoji{
.image = CrownMask(place),
.margin = st::paidReactCrownMargin,
})).first;
}
result.append(Ui::Text::SingleCustomEmoji(i->second)).append(' ');
}
result.append(tr::bold(peer->shortName()));
return result;
}
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::on_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::on_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::updateLeftFade() {
const auto leftFadeShown = (_pinnedScroll->scrollLeft() > 0);
if (_leftFadeShown != leftFadeShown) {
_leftFadeShown = leftFadeShown;
//const auto from = leftFadeShown ? 0. : 1.;
//const auto till = rightFadeShown ? 1. : 0.;
//_leftFadeAnimation.start([=] {
_pinned->update(
_pinnedScroll->scrollLeft(),
0,
_fadeWidth,
_pinned->height());
//}, from, till, st::slideWrapDuration);
}
}
void MessagesUi::updateRightFade() {
const auto max = _pinnedScroll->scrollLeftMax();
const auto rightFadeShown = (_pinnedScroll->scrollLeft() < max);
if (_rightFadeShown != rightFadeShown) {
_rightFadeShown = rightFadeShown;
//const auto from = rightFadeShown ? 0. : 1.;
//const auto till = rightFadeShown ? 1. : 0.;
//_rightFadeAnimation.start([=] {
_pinned->update(
(_pinnedScroll->scrollLeft()
+ _pinnedScroll->width()
- _fadeWidth),
0,
_fadeWidth,
_pinned->height());
//}, 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));
_messages->move(0, 0);
rpl::combine(
scroll->scrollTopValue(),
scroll->heightValue(),
_messages->heightValue()
) | rpl::on_next([=] {
updateTopFade();
updateBottomFade();
}, scroll->lifetime());
if (_mode == MessagesMode::GroupCall) {
receiveSomeMouseEvents();
} else {
receiveAllMouseEvents();
}
_messages->paintRequest() | rpl::on_next([=](QRect clip) {
const auto start = scroll->scrollTop();
const auto end = start + scroll->height();
const auto ratio = style::DevicePixelRatio();
const auto session = &_show->session();
const auto &colorings = session->appConfig().groupCallColorings();
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.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);
}
auto bg = (std::unique_ptr<PayedBg>*)nullptr;
if (!_streamMode) {
_messageBgRect.paint(p, { x, y, width, use });
} else if (entry.stars) {
const auto coloring = Ui::StarsColoringForCount(
colorings,
entry.stars);
bg = &_bgs[ColoringKey(coloring)];
if (!*bg) {
*bg = std::make_unique<PayedBg>(coloring);
}
p.setOpacity(kColoredMessageBgOpacity);
(*bg)->messageLight.paint(p, { x, y, width, use });
p.setOpacity(1.);
if (_highlightAnimation.animating()
&& entry.id == _highlightId) {
const auto radius = CountMessageRadius();
const auto progress = _highlightAnimation.value(3.);
p.setBrush(st::white);
p.setOpacity(
std::min((1.5 - std::abs(1.5 - progress)), 1.));
auto hq = PainterHighQualityEnabler(p);
p.drawRoundedRect(x, y, width, use, radius, radius);
p.setOpacity(1.);
}
} else if (entry.admin) {
_messageBgRect.paint(p, { x, y, width, use });
}
const auto textLeft = entry.textLeft;
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.);
}
}
p.setPen(st::white);
if (!entry.name.isEmpty()) {
const auto space = st::normalFont->spacew;
entry.name.draw(p, {
.position = {
x + textLeft,
y + padding.top(),
},
.availableWidth = entry.nameWidth,
.palette = &st::groupCallMessagePalette,
.elisionLines = 1,
});
const auto liveLeft = x + textLeft + entry.nameWidth + space;
_liveBadge.draw(p, {
.position = { liveLeft, y + padding.top() },
});
p.setOpacity(kAdminBadgeTextOpacity);
const auto adminLeft = x
+ entry.width
- padding.right()
- _adminBadge.maxWidth();
_adminBadge.draw(p, {
.position = { adminLeft, y + padding.top() },
});
p.setOpacity(1.);
}
const auto pricePadding = st::groupCallPricePadding;
const auto textRight = padding.right()
+ (entry.simple
? (entry.price.maxWidth()
+ pricePadding.left()
+ pricePadding.right())
: 0);
entry.text.draw(p, {
.position = {
x + textLeft,
y + entry.textTop,
},
.availableWidth = entry.width - textLeft - textRight,
.palette = &st::groupCallMessagePalette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = now,
.paused = !_messages->window()->isActiveWindow(),
});
if (!entry.price.isEmpty()) {
const auto priceRight = x
+ entry.width
- entry.price.maxWidth();
const auto priceLeft = entry.simple
? (priceRight
- (padding.top() - pricePadding.top())
- pricePadding.right())
: (priceRight - (padding.right() / 2));
const auto priceTop = entry.simple
? (y + entry.textTop)
: (y + use - st::normalFont->height);
if (entry.simple && bg) {
p.setOpacity(kDarkOverOpacity);
const auto r = QRect(
priceLeft,
priceTop,
entry.price.maxWidth(),
st::normalFont->height
).marginsAdded(pricePadding);
(*bg)->priceDark.paint(p, r);
p.setOpacity(1.);
}
entry.price.draw(p, {
.position = { priceLeft, priceTop },
.availableWidth = entry.price.maxWidth(),
});
}
if (!scaled && entry.reactionId && !entry.reactionAnimation) {
startReactionAnimation(entry);
}
if (scaled) {
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));
}, _messages->lifetime());
scroll->show();
applyGeometry();
}
void MessagesUi::receiveSomeMouseEvents() {
ReceiveSomeMouseEvents(_scroll.get(), [=](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()) {
handleClick(entry, point);
return true;
}
break;
}
return false;
});
}
void MessagesUi::receiveAllMouseEvents() {
_messages->events() | rpl::on_next([=](not_null<QEvent*> e) {
const auto type = e->type();
if (type != QEvent::MouseButtonPress) {
return;
}
const auto m = static_cast<QMouseEvent*>(e.get());
const auto point = m->pos();
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()) {
if (m->button() == Qt::LeftButton) {
handleClick(entry, point);
} else {
showContextMenu(entry, m->globalPos());
}
}
break;
}
}, _messages->lifetime());
}
void MessagesUi::handleClick(const MessageView &entry, QPoint point) {
const auto padding = st::groupCallMessagePadding;
const auto userpicSize = st::groupCallUserpic;
const auto userpicPadding = st::groupCallUserpicPadding;
const auto userpic = QRect(
entry.left + userpicPadding.left(),
entry.top + userpicPadding.top(),
userpicSize,
userpicSize);
const auto name = entry.name.isEmpty()
? QRect()
: QRect(
entry.left + entry.textLeft,
entry.top + padding.top(),
entry.nameWidth,
st::messageTextStyle.font->height);
const auto link = (userpic.contains(point) || name.contains(point))
? entry.fromLink
: entry.text.getState(point - QPoint(
entry.left + entry.textLeft,
entry.top + entry.textTop
), entry.width - entry.textLeft - padding.right()).link;
if (link) {
ActivateClickHandler(_messages, link, Qt::LeftButton);
}
}
void MessagesUi::showContextMenu(
const MessageView &entry,
QPoint globalPoint) {
if (_menu || !entry.date || entry.failed) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(
_parent,
st::groupCallPopupMenuWithIcons);
_menu->addAction(MakeMessageDateAction(_menu.get(), entry.date));
const auto &original = entry.original;
const auto canCopy = !original.empty();
const auto canDelete = entry.mine || _canManage.current();
if (canCopy || canDelete) {
_menu->addSeparator(&st::mediaviewWideMenuSeparator);
}
if (canCopy) {
_menu->addAction(tr::lng_context_copy_text(tr::now), [=] {
TextUtilities::SetClipboardText(
TextForMimeData::WithExpandedLinks(original));
}, &st::mediaMenuIconCopy);
}
if (canDelete) {
const auto id = entry.id;
const auto from = entry.from;
const auto canModerate = _canManage.current() && !entry.mine;
_menu->addAction(tr::lng_context_delete_msg(tr::now), [=] {
ShowDeleteMessageConfirmation(_show, id, from, canModerate, [=](
MessageDeleteRequest request) {
_deleteRequests.fire_copy(request);
});
}, &st::mediaMenuIconDelete);
}
_menu->popup(globalPoint);
}
void MessagesUi::setupPinnedWidget() {
_pinnedScroll = std::make_unique<Ui::ElasticScroll>(
_parent,
st::groupCallMessagesScroll,
Qt::Horizontal);
const auto scroll = _pinnedScroll.get();
_pinned = scroll->setOwnedWidget(object_ptr<Ui::RpWidget>(scroll));
_pinned->move(0, 0);
rpl::combine(
scroll->scrollLeftValue(),
scroll->widthValue(),
_pinned->widthValue()
) | rpl::on_next([=] {
updateLeftFade();
updateRightFade();
}, scroll->lifetime());
struct Animation {
base::Timer seconds;
Ui::Animations::Simple smooth;
bool requiresSmooth = false;
};
const auto animation = _pinned->lifetime().make_state<Animation>();
animation->seconds.setCallback([=] {
const auto now = crl::now();
auto smooth = false;
auto off = base::flat_set<MsgId>();
for (auto &entry : _pinnedViews) {
if (entry.removed) {
continue;
} else if (entry.end <= now) {
off.emplace(entry.id);
entry.requiresSmooth = false;
} else if (entry.requiresSmooth) {
smooth = true;
}
}
if (smooth && !anim::Disabled()) {
animation->smooth.start([=] {
_pinned->update();
}, 0., 1., 900.);
} else {
_pinned->update();
}
for (const auto &id : off) {
const auto i = ranges::find(_pinnedViews, id, &PinnedView::id);
if (i != end(_pinnedViews)) {
togglePinned(*i, false);
}
}
});
animation->seconds.callEach(crl::time(1000));
_pinned->paintRequest() | rpl::on_next([=](QRect clip) {
const auto session = &_show->session();
const auto &colorings = session->appConfig().groupCallColorings();
const auto start = scroll->scrollLeft();
const auto end = start + scroll->width();
const auto ratio = style::DevicePixelRatio();
if ((_pinnedCanvas.width() < scroll->width() * ratio)
|| (_pinnedCanvas.height() < scroll->height() * ratio)) {
_pinnedCanvas = QImage(
scroll->size() * ratio,
QImage::Format_ARGB32_Premultiplied);
_pinnedCanvas.setDevicePixelRatio(ratio);
}
auto p = Painter(&_pinnedCanvas);
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::groupCallPinnedPadding;
p.translate(-start, 0);
for (auto &entry : _pinnedViews) {
if (entry.width <= skip || entry.left + entry.width <= start) {
continue;
} else if (entry.left >= end) {
break;
}
const auto x = entry.left;
const auto y = entry.top;
const auto use = entry.realWidth - skip;
const auto height = entry.height;
p.setPen(Qt::NoPen);
const auto scaled = (entry.width < entry.realWidth);
if (scaled) {
const auto used = entry.width - skip;
const auto mx = scaled ? (x + (used / 2)) : 0;
const auto my = scaled ? (y + (height / 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);
}
const auto coloring = Ui::StarsColoringForCount(
colorings,
entry.stars);
auto &bg = _bgs[ColoringKey(coloring)];
if (!bg) {
bg = std::make_unique<PayedBg>(coloring);
}
const auto still = (entry.end - now) / float64(entry.duration);
const auto part = int(base::SafeRound(still * use));
const auto line = st::lineWidth;
if (part > 0) {
p.setOpacity(kColoredMessageBgOpacity);
bg->pinnedLight.paint(p, { x, y, use, height });
}
if (part < use) {
p.setClipRect(x + part, y, use - part + line, height);
p.setOpacity(kDarkOverOpacity);
bg->pinnedDark.paint(p, { x, y, use, height });
}
p.setClipping(false);
p.setOpacity(1.);
const auto userpicSize = st::groupCallPinnedUserpic;
const auto userpicPadding = st::groupCallUserpicPadding;
const auto position = QPoint(
x + userpicPadding.left(),
y + userpicPadding.top());
entry.from->paintUserpic(p, entry.view, {
.position = position,
.size = userpicSize,
.shape = Ui::PeerUserpicShape::Circle,
});
const auto leftSkip = userpicPadding.left()
+ userpicSize
+ userpicPadding.right();
p.setPen(st::white);
entry.text.draw(p, {
.position = { x + leftSkip, y + padding.top() },
.availableWidth = entry.width - leftSkip - padding.right(),
.elisionLines = 1,
});
if (scaled) {
p.restore();
}
}
p.translate(start, 0);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p.setPen(Qt::NoPen);
const auto leftFade = (//_leftFadeAnimation.value(
_leftFadeShown ? 1. : 0.);
if (leftFade) {
auto gradientLeft = QLinearGradient(0, 0, _fadeWidth, 0);
gradientLeft.setStops({
{ 0., QColor(255, 255, 255, 0) },
{ 1., QColor(255, 255, 255, 255) },
});
p.setOpacity(leftFade);
p.setBrush(gradientLeft);
p.drawRect(0, 0, _fadeWidth, scroll->height());
p.setOpacity(1.);
}
const auto rightFade = (//_rightFadeAnimation.value(
_rightFadeShown ? 1. : 0.);
if (rightFade) {
const auto till = scroll->width();
const auto from = till - _fadeWidth;
auto gradientRight = QLinearGradient(from, 0, till, 0);
gradientRight.setStops({
{ 0., QColor(255, 255, 255, 255) },
{ 1., QColor(255, 255, 255, 0) },
});
p.setBrush(gradientRight);
p.drawRect(from, 0, _fadeWidth, scroll->height());
}
QPainter(_pinned).drawImage(
QRect(QPoint(start, 0), scroll->size()),
_pinnedCanvas,
QRect(QPoint(), scroll->size() * ratio));
}, _pinned->lifetime());
_pinned->setMouseTracking(true);
const auto find = [=](QPoint position) {
if (position.y() < 0 || position.y() >= _pinned->height()) {
return MsgId();
} else for (const auto &entry : _pinnedViews) {
if (entry.left > position.x()) {
break;
} else if (entry.left + entry.width > position.x()) {
return entry.id;
}
}
return MsgId();
};
_pinned->events() | rpl::on_next([=](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::MouseButtonPress) {
const auto pos = static_cast<QMouseEvent*>(e.get())->pos();
if (const auto id = find(pos)) {
if (_hidden) {
showList(*base::take(_hidden));
_hiddenShowRequested.fire({});
}
highlightMessage(id);
}
} else if (type == QEvent::MouseMove) {
const auto pos = static_cast<QMouseEvent*>(e.get())->pos();
_pinned->setCursor(find(pos)
? style::cur_pointer
: style::cur_default);
}
}, _pinned->lifetime());
scroll->show();
applyGeometry();
}
void MessagesUi::highlightMessage(MsgId id) {
if (!_scroll) {
return;
}
const auto i = ranges::find(_views, id, &MessageView::id);
if (i == end(_views) || i->top < 0) {
return;
} else if (i->toggleAnimation.animating()) {
_delayedHighlightId = id;
return;
}
_delayedHighlightId = 0;
const auto top = std::clamp(
i->top - ((_scroll->height() - i->realHeight) / 2),
0,
i->top);
const auto to = top - i->top;
const auto from = _scroll->scrollTop() - i->top;
if (from == to) {
startHighlight(id);
return;
}
_scrollToAnimation.stop();
_scrollToAnimation.start([=] {
const auto i = ranges::find(_views, id, &MessageView::id);
if (i == end(_views)) {
_scrollToAnimation.stop();
return;
}
_scroll->scrollToY(i->top + _scrollToAnimation.value(to));
if (!_scrollToAnimation.animating()) {
startHighlight(id);
}
}, from, to, st::slideDuration, anim::easeOutCirc);
}
void MessagesUi::startHighlight(MsgId id) {
_highlightId = id;
_highlightAnimation.start([=] {
repaintMessage(id);
}, 0., 3., 1000);
}
void MessagesUi::applyGeometry() {
if (_scroll) {
auto top = 0;
for (auto &entry : _views) {
entry.top = top;
updateMessageSize(entry);
updateMessageHeight(entry);
top += entry.height;
}
}
applyGeometryToPinned();
updateGeometries();
}
void MessagesUi::applyGeometryToPinned() {
if (!_pinnedScroll) {
setPinnedScrollSkip(0);
return;
}
const auto skip = st::groupCallMessageSkip;
auto maxHeight = 0;
auto left = 0;
for (auto &entry : _pinnedViews) {
entry.left = left;
updatePinnedSize(entry);
updatePinnedWidth(entry);
left += entry.width;
if (maxHeight < entry.height + skip) {
const auto possible = countPinnedScrollSkip(entry);
maxHeight = std::max(possible, maxHeight);
}
}
setPinnedScrollSkip(maxHeight);
}
int MessagesUi::countPinnedScrollSkip(const PinnedView &entry) const {
const auto skip = st::groupCallMessageSkip;
if (!entry.toggleAnimation.animating()) {
return entry.height + skip;
}
const auto used = ((entry.height + skip) * entry.width)
/ float64(entry.realWidth);
return int(base::SafeRound(used));
}
void MessagesUi::setPinnedScrollSkip(int skip) {
if (_pinnedScrollSkip != skip) {
_pinnedScrollSkip = skip;
updateGeometries();
}
}
void MessagesUi::updateGeometries() {
if (_pinnedScroll) {
const auto skip = st::groupCallMessageSkip;
const auto width = _pinnedViews.empty()
? 0
: (_pinnedViews.back().left + _pinnedViews.back().width - skip);
const auto height = _pinnedViews.empty()
? 0
: _pinnedViews.back().height;
const auto bottom = _bottom - st::groupCallMessageSkip;
_pinned->resize(width, height);
const auto min = std::min(width, _width);
_pinnedScroll->setGeometry(_left, bottom - height, min, height);
}
if (_scroll) {
const auto scrollBottom = (_scroll->scrollTop() + _scroll->height());
const auto atBottom = (scrollBottom >= _messages->height());
const auto bottom = _bottom - _pinnedScrollSkip;
const auto height = _views.empty()
? 0
: (_views.back().top + _views.back().height);
_messages->resize(_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) {
const auto min = st::groupCallWidth * 2 / 6;
if (width < min) {
const auto add = min - width;
width += add;
left -= add / 2;
}
if (_left != left
|| _bottom != bottom
|| _width != width
|| _availableHeight != availableHeight) {
_left = left;
_bottom = bottom;
_width = width;
_availableHeight = availableHeight;
applyGeometry();
}
}
void MessagesUi::raise() {
if (_scroll) {
_scroll->raise();
}
for (const auto &view : _views) {
if (const auto widget = view.reactionWidget.get()) {
widget->raise();
}
}
}
rpl::producer<> MessagesUi::hiddenShowRequested() const {
return _hiddenShowRequested.events();
}
rpl::producer<MessageDeleteRequest> MessagesUi::deleteRequests() const {
return _deleteRequests.events();
}
rpl::lifetime &MessagesUi::lifetime() {
return _lifetime;
}
} // namespace Calls::Group