/* 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 #include 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 scroll, Fn handleClick) { class EventFilter final : public QObject { public: explicit EventFilter( not_null scroll, Fn handleClick) : QObject(scroll) , _handleClick(std::move(handleClick)) { } bool eventFilter(QObject *watched, QEvent *event) { if (event->type() == QEvent::MouseButtonPress) { return mousePressFilter( watched, static_cast(event)); } else if (event->type() == QEvent::Wheel) { return wheelFilter( watched, static_cast(event)); } return false; } bool mousePressFilter( QObject *watched, not_null event) { Expects(parent()->isWidgetType()); const auto scroll = static_cast(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 event) { Expects(parent()->isWidgetType()); const auto scroll = static_cast(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 _handleClick; }; scroll->setAttribute(Qt::WA_TransparentForMouseEvents); qApp->installEventFilter( new EventFilter(scroll, std::move(handleClick))); } [[nodiscard]] base::unique_qptr MakeMessageDateAction( not_null 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( menu->menu(), st::storiesCommentSentAt, action, nullptr, nullptr); } void ShowDeleteMessageConfirmation( std::shared_ptr show, MsgId id, not_null from, bool canModerate, Fn callback) { show->show(Box([=](not_null box) { struct State { rpl::variable report; rpl::variable all; rpl::variable ban; }; const auto state = box->lifetime().make_state(); const auto confirmed = [=](Fn 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 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( 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 from; ClickHandlerPtr fromLink; Ui::Animations::Simple toggleAnimation; Ui::Animations::Simple sentAnimation; Data::ReactionId reactionId; std::unique_ptr sendingAnimation; std::unique_ptr reactionAnimation; std::unique_ptr 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 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 parent, std::shared_ptr show, MessagesMode mode, rpl::producer> messages, rpl::producer>> topDonorsValue, rpl::producer idUpdates, rpl::producer canManageValue, rpl::producer 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> messages, rpl::producer shown) { rpl::combine( std::move(messages), std::move(shown) ) | rpl::on_next([=](std::vector &&list, bool shown) { if (shown) { _hidden = std::nullopt; } else { _hidden = base::take(list); } showList(list); }, _lifetime); } void MessagesUi::showList(const std::vector &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 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::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([=] { _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( 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::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 peer) const { const auto &donors = _topDonors.current(); const auto i = ranges::find(donors, peer); if (i == end(donors)) { return 0; } return static_cast(std::distance(begin(donors), i)) + 1; } TextWithEntities MessagesUi::nameText( not_null 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(_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( &_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 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( _parent, st::groupCallMessagesScroll); const auto scroll = _scroll.get(); _messages = scroll->setOwnedWidget(object_ptr(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*)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(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 e) { const auto type = e->type(); if (type != QEvent::MouseButtonPress) { return; } const auto m = static_cast(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( _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( _parent, st::groupCallMessagesScroll, Qt::Horizontal); const auto scroll = _pinnedScroll.get(); _pinned = scroll->setOwnedWidget(object_ptr(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->seconds.setCallback([=] { const auto now = crl::now(); auto smooth = false; auto off = base::flat_set(); 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(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 e) { const auto type = e->type(); if (type == QEvent::MouseButtonPress) { const auto pos = static_cast(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(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 MessagesUi::deleteRequests() const { return _deleteRequests.events(); } rpl::lifetime &MessagesUi::lifetime() { return _lifetime; } } // namespace Calls::Group