diff --git a/Telegram/Resources/icons/calls/filled_stream_crown.svg b/Telegram/Resources/icons/calls/filled_stream_crown.svg new file mode 100644 index 0000000000..f1b17ee2be --- /dev/null +++ b/Telegram/Resources/icons/calls/filled_stream_crown.svg @@ -0,0 +1,7 @@ + + + Filled / filled_stream_crown + + + + \ No newline at end of file diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index b12071d3ab..ec6f5e39df 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -1721,6 +1721,11 @@ groupCallPinnedPadding: margins(10px, 4px, 10px, 2px); groupCallPinnedMaxWidth: 96px; groupCallPinnedUserpic: 22px; +groupCallTopReactorBadge: RoundCheckbox(defaultRoundCheckbox) { + width: 1px; + border: groupCallMembersBg; +} + confcallLinkMenu: IconButton(boxTitleClose) { icon: icon {{ "title_menu_dots", boxTitleCloseFg }}; iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }}; diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp b/Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp index 73e96d19b5..1f5f48a2e3 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp @@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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" @@ -245,6 +246,31 @@ void ShowDeleteMessageConfirmation( })); } +[[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 { @@ -266,6 +292,7 @@ struct MessagesUi::MessageView { Ui::Text::String price; TimeId date = 0; int stars = 0; + int place = 0; int top = 0; int width = 0; int left = 0; @@ -290,6 +317,7 @@ struct MessagesUi::PinnedView { crl::time duration = 0; crl::time end = 0; int stars = 0; + int place = 0; int top = 0; int width = 0; int left = 0; @@ -313,6 +341,7 @@ MessagesUi::MessagesUi( std::shared_ptr show, MessagesMode mode, rpl::producer> messages, + rpl::producer>> topDonorsValue, rpl::producer idUpdates, rpl::producer canManageValue, rpl::producer shown) @@ -326,6 +355,8 @@ MessagesUi::MessagesUi( 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) { @@ -350,6 +381,27 @@ void MessagesUi::setupBadges() { helper.context()); _adminBadge.setText(st::messageTextStyle, tr::lng_admin_badge(tr::now)); + + _topDonors.value( + ) | rpl::start_with_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( @@ -577,17 +629,17 @@ void MessagesUi::setContentFailed(MessageView &entry) { entry.price = Ui::Text::String(); } -void MessagesUi::setContent( - MessageView &entry, - const QString &name, - const TextWithEntities &text, - int stars) { +void MessagesUi::setContent(MessageView &entry) { + const auto name = nameText(entry.from, entry.place); entry.name = entry.admin ? Ui::Text::String( st::messageTextStyle, - Ui::Text::Bold(name)) + name, + kMarkupTextOptions, + Ui::kQFixedMax, + _crownHelper.context()) : Ui::Text::String(); - if (stars) { + if (const auto stars = entry.stars) { entry.price = Ui::Text::String( st::whoReadDateStyle, Ui::Text::IconEmoji( @@ -598,17 +650,14 @@ void MessagesUi::setContent( entry.price = Ui::Text::String(); } const auto composed = entry.admin - ? text - : Ui::Text::Link(Ui::Text::Bold(name), 1).append(' ').append(text); + ? entry.original + : Ui::Text::Link(name, 1).append(' ').append(entry.original); entry.text = Ui::Text::String( st::messageTextStyle, composed, kMarkupTextOptions, st::groupCallWidth / 8, - Core::TextContext({ - .session = &_show->session(), - .repaint = [this, id = entry.id] { repaintMessage(id); }, - })); + _crownHelper.context([this, id = entry.id] { repaintMessage(id); })); if (!entry.price.isEmpty()) { entry.text.updateSkipBlock( entry.price.maxWidth(), @@ -635,6 +684,15 @@ void MessagesUi::setContent( } } +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; @@ -711,6 +769,7 @@ void MessagesUi::appendMessage(const Message &data) { .original = data.text, .date = data.date, .stars = data.stars, + .place = donorPlace(peer), .top = top, .sending = !data.date, .admin = data.admin && _streamMode, @@ -726,7 +785,7 @@ void MessagesUi::appendMessage(const Message &data) { if (data.failed) { setContentFailed(entry); } else { - setContent(entry, data.peer->shortName(), data.text, data.stars); + setContent(entry); } updateMessageSize(entry); if (entry.sending) { @@ -755,13 +814,12 @@ void MessagesUi::togglePinned(PinnedView &entry, bool shown) { } void MessagesUi::repaintPinned(MsgId id) { - auto i = ranges::find(_pinnedViews, id, &PinnedView::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; - i = _pinnedViews.erase(i); - recountWidths(i, left); + recountWidths(_pinnedViews.erase(i), left); return; } if (i->toggleAnimation.animating() || i->width != i->realWidth) { @@ -808,8 +866,6 @@ void MessagesUi::appendPinned(const Message &data, TimeId now) { data.id, &PinnedView::id)) { return; - } else if (!_pinnedScroll) { - setupPinnedWidget(); } const auto id = data.id; @@ -821,7 +877,12 @@ void MessagesUi::appendPinned(const Message &data, TimeId now) { if (i->end > finishes) { return; } - _pinnedViews.erase(i); + const auto left = i->left; + recountWidths(_pinnedViews.erase(i), left); + } + + if (!_pinnedScroll) { + setupPinnedWidget(); } const auto j = ranges::lower_bound( _pinnedViews, @@ -839,19 +900,47 @@ void MessagesUi::appendPinned(const Message &data, TimeId now) { .duration = (data.pinFinishDate - data.date) * crl::time(1000), .end = finishes, .stars = data.stars, + .place = donorPlace(peer), .left = left, }); const auto repaint = [=] { repaintPinned(id); }; - entry.text.setMarkedText( - st::messageTextStyle, - Ui::Text::Bold(data.peer->shortName())); + 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(Ui::Text::Bold(peer->shortName())); + return result; +} + void MessagesUi::checkReactionContent( MessageView &entry, const TextWithEntities &text) { @@ -1669,6 +1758,7 @@ void MessagesUi::applyGeometryToPinned() { auto left = 0; for (auto &entry : _pinnedViews) { entry.left = left; + updatePinnedSize(entry); updatePinnedWidth(entry); left += entry.width; diff --git a/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h b/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h index 6aeaff39de..c661004fc2 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h +++ b/Telegram/SourceFiles/calls/group/calls_group_messages_ui.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unique_qptr.h" #include "ui/effects/animations.h" +#include "ui/text/custom_emoji_helper.h" #include "ui/round_rect.h" struct TextWithTags; @@ -54,6 +55,7 @@ public: std::shared_ptr show, MessagesMode mode, rpl::producer> messages, + rpl::producer>> topDonorsValue, rpl::producer idUpdates, rpl::producer canManageValue, rpl::producer shown); @@ -89,11 +91,8 @@ private: void handleIdUpdates(rpl::producer idUpdates); void toggleMessage(MessageView &entry, bool shown); void setContentFailed(MessageView &entry); - void setContent( - MessageView &entry, - const QString &name, - const TextWithEntities &text, - int stars); + void setContent(MessageView &entry); + void setContent(PinnedView &entry); void updateMessageSize(MessageView &entry); bool updateMessageHeight(MessageView &entry); void updatePinnedSize(PinnedView &entry); @@ -134,6 +133,11 @@ private: void handleClick(const MessageView &entry, QPoint point); void showContextMenu(const MessageView &entry, QPoint globalPoint); + [[nodiscard]] int donorPlace(not_null peer) const; + [[nodiscard]] TextWithEntities nameText( + not_null peer, + int place); + const not_null _parent; const std::shared_ptr _show; const MessagesMode _mode; @@ -168,6 +172,9 @@ private: Ui::Text::String _liveBadge; Ui::Text::String _adminBadge; + Ui::Text::CustomEmojiHelper _crownHelper; + base::flat_map _crownEmojiDataCache; + rpl::variable>> _topDonors; //Ui::Animations::Simple _topFadeAnimation; //Ui::Animations::Simple _bottomFadeAnimation; //Ui::Animations::Simple _leftFadeAnimation; diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index d269cde8e2..1905327f7b 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -243,6 +243,7 @@ Panel::Panel(not_null call, ConferencePanelMigration info) uiShow(), MessagesMode::GroupCall, _call->messages()->listValue(), + nullptr, _call->messages()->idUpdates(), _call->canManageValue(), _call->messagesEnabledValue())) diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 28a3a02879..a60d4026ac 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -43,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "media/stories/media_stories_controller.h" #include "media/stories/media_stories_stealth.h" +#include "media/view/media_view_video_stream.h" #include "menu/menu_send.h" #include "payments/ui/payments_reaction_box.h" // MaxTopPaidDonorsShown #include "settings/settings_credits_graphics.h" // DarkCreditsEntryBoxStyle @@ -1014,31 +1015,7 @@ void ReplyArea::updateVideoStream(not_null videoStream) { _type = ReplyAreaType::VideoStreamComment; _videoStream = videoStream; const auto messages = videoStream->messages(); - _controls->setStarsReactionTop(rpl::single(rpl::empty) | rpl::then( - messages->starsValueChanges() - ) | rpl::map([=] { - const auto &list = messages->starsTop().topDonors; - const auto peer = _videoStream->peer(); - const auto &as = peer->session().sendAsPeers().list( - { peer, Main::SendAsType::VideoStream }); - const auto limit = std::min( - int(list.size()), - Ui::MaxTopPaidDonorsShown() + std::max(int(as.size()), 1)); - auto still = Ui::MaxTopPaidDonorsShown(); - auto result = std::vector(); - result.reserve(limit); - for (const auto &item : list) { - result.push_back({ - .peer = item.peer, - .count = uint32(item.stars), - .my = item.my ? 1U : 0U, - }); - if (!item.my && !--still) { - break; - } - } - return result; - })); + _controls->setStarsReactionTop(View::TopVideoStreamDonors(videoStream)); } bool ReplyArea::showSlowmodeError() { diff --git a/Telegram/SourceFiles/media/view/media_view_video_stream.cpp b/Telegram/SourceFiles/media/view/media_view_video_stream.cpp index 7ff3e93ce0..3fdcccc9e5 100644 --- a/Telegram/SourceFiles/media/view/media_view_video_stream.cpp +++ b/Telegram/SourceFiles/media/view/media_view_video_stream.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/view/media_view_video_stream.h" +#include "data/data_message_reactions.h" #include "calls/group/calls_group_call.h" #include "calls/group/calls_group_common.h" #include "calls/group/calls_group_members.h" @@ -17,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/compose/compose_show.h" #include "core/application.h" #include "core/core_settings.h" +#include "payments/ui/payments_reaction_box.h" #include "ui/effects/path_shift_gradient.h" #include "ui/painter.h" #include "styles/style_calls.h" @@ -59,6 +61,49 @@ private: }; +auto TopVideoStreamDonors(not_null call) +-> rpl::producer> { + const auto messages = call->messages(); + return rpl::single(rpl::empty) | rpl::then( + messages->starsValueChanges() + ) | rpl::map([=] { + const auto &list = messages->starsTop().topDonors; + const auto peer = call->peer(); + auto still = Ui::MaxTopPaidDonorsShown(); + auto result = std::vector(); + result.reserve(list.size()); + for (const auto &item : list) { + result.push_back({ + .peer = item.peer, + .count = uint32(item.stars), + .my = item.my ? 1U : 0U, + }); + if (!item.my && !--still) { + break; + } + } + return result; + }); +} + +auto TopDonorPlaces(not_null call) +-> rpl::producer>> { + return TopVideoStreamDonors( + call + ) | rpl::map([=](const std::vector &lst) { + auto result = std::vector>(); + auto left = Ui::MaxTopPaidDonorsShown(); + result.reserve(lst.size()); + for (const auto &donor : lst) { + result.push_back(donor.peer); + if (!--left) { + break; + } + } + return result; + }); +} + VideoStream::Delegate::Delegate(Fn close) : _close(std::move(close)) { } @@ -190,6 +235,7 @@ VideoStream::VideoStream( _show, Calls::Group::MessagesMode::VideoStream, _call->messages()->listValue(), + TopDonorPlaces(_call.get()), _call->messages()->idUpdates(), _call->canManageValue(), rpl::combine( diff --git a/Telegram/SourceFiles/media/view/media_view_video_stream.h b/Telegram/SourceFiles/media/view/media_view_video_stream.h index 81c135c8be..d4ff64c1ae 100644 --- a/Telegram/SourceFiles/media/view/media_view_video_stream.h +++ b/Telegram/SourceFiles/media/view/media_view_video_stream.h @@ -26,6 +26,7 @@ class Show; namespace Data { class GroupCall; +struct MessageReactionsTopPaid; } // namespace Data namespace Ui { @@ -38,6 +39,9 @@ enum class Backend; namespace Media::View { +[[nodiscard]] auto TopVideoStreamDonors(not_null call) +-> rpl::producer>; + class VideoStream final { public: VideoStream( diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index ece6380237..cd6c6da494 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -26,10 +26,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/dynamic_image.h" #include "ui/painter.h" #include "ui/vertical_list.h" +#include "styles/style_calls.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_credits.h" #include "styles/style_info.h" +#include "styles/style_info_levels.h" #include "styles/style_layers.h" #include "styles/style_media_player.h" #include "styles/style_premium.h" @@ -237,7 +239,8 @@ void PaidReactionSlider( count ).bgLight) : st::creditsBg3->c), - videoStream ? st::white->c : st::premiumButtonFg->c); + videoStream ? st::white->c : st::premiumButtonFg->c, + videoStream ? &st::groupCallTopReactorBadge : nullptr); } void AddArrowDown(not_null widget) { @@ -267,10 +270,19 @@ void AddArrowDown(not_null widget) { [[nodiscard]] not_null MakeTopReactor( not_null parent, const PaidReactionTop &data, + int place, const std::vector &colorings, Fn selectShownPeer, bool videoStream) { + auto top = 0; + auto height = st::paidReactTopNameSkip + st::normalFont->height; + if (videoStream) { + top += st::paidReactCrownSkip; + height += top; + } + const auto result = CreateChild(parent); + result->resize(0, height); result->show(); if (data.click && !data.my) { result->setClickedCallback(data.click); @@ -300,7 +312,7 @@ void AddArrowDown(not_null widget) { result->paintRequest() | rpl::start_with_next([=] { auto p = Painter(result); const auto left = (result->width() - st::paidReactTopUserpic) / 2; - p.drawImage(left, 0, photo->image(st::paidReactTopUserpic)); + p.drawImage(left, top, photo->image(st::paidReactTopUserpic)); if (state->badge.isNull()) { state->badge = GenerateBadgeImage(colorings, count, videoStream); @@ -309,12 +321,36 @@ void AddArrowDown(not_null widget) { / state->badge.devicePixelRatio(); p.drawImage( (result->width() - bwidth) / 2, - st::paidReactTopBadgeSkip, + top + st::paidReactTopBadgeSkip, state->badge); - p.setPen(videoStream ? st::groupCallMembersFg : st::windowFg); + if (videoStream) { + const auto bg = Calls::Group::Ui::StarsColoringForCount( + colorings, + count + ).bgLight; + const auto &icon = st::paidReactCrown; + const auto left = (result->width() - icon.width()) / 2; + const auto shift = st::paidReactCrownOutline; + const auto outline = st::groupCallMembersBg->c; + icon.paint(p, left - shift, shift, result->width(), outline); + icon.paint(p, left + shift, shift, result->width(), outline); + icon.paint(p, left, 0, result->width(), bg); + + const auto top = st::paidReactCrownTop; + p.setPen(st::white); + p.setFont(st::levelStyle.font); + p.drawText( + QRect(left, top, icon.width(), icon.height()), + QString::number(place), + style::al_top); + + p.setPen(st::groupCallMembersFg); + } else { + p.setPen(st::windowFg); + } const auto skip = st::normalFont->spacew; - const auto nameTop = st::paidReactTopNameSkip; + const auto nameTop = top + st::paidReactTopNameSkip; const auto available = result->width() - skip * 2; state->name.draw(p, skip, nameTop, available, style::al_top); }, result->lifetime()); @@ -388,11 +424,10 @@ void FillTopReactors( [](QRect) { return st::creditsBg3->b; }), st::boxRowPadding + st::paidReactTopTitleMargin), style::al_top); - const auto height = st::paidReactTopNameSkip + st::normalFont->height; const auto wrap = container->add( object_ptr>( container, - object_ptr(container, height), + object_ptr(container, 0), st::paidReactTopMargin)); const auto parent = wrap->entity(); using Key = TopReactorKey; @@ -461,6 +496,7 @@ void FillTopReactors( widget->hide(); } state->widgets.clear(); + auto index = 0; for (const auto &entry : list) { const auto key = Key{ .photo = entry.photo, @@ -473,6 +509,7 @@ void FillTopReactors( : MakeTopReactor( parent, entry, + ++index, colorings, selectShownPeer, videoStream); @@ -498,6 +535,9 @@ void FillTopReactors( state->updated.events_starting_with({}), wrap->widthValue() ) | rpl::start_with_next([=](auto, int width) { + if (!state->widgets.empty()) { + parent->resize(parent->width(), state->widgets.back()->height()); + } const auto single = width / 4; if (single <= st::paidReactTopUserpic) { return; @@ -505,7 +545,7 @@ void FillTopReactors( const auto count = int(state->widgets.size()); auto left = (width - single * count) / 2; for (const auto &widget : state->widgets) { - widget->setGeometry(left, 0, single, height); + widget->setGeometry(left, 0, single, widget->height()); left += single; } }, wrap->lifetime()); diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index bbaee12c9d..641914865c 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -447,6 +447,13 @@ paidReactTopMargin: margins(0px, 12px, 0px, 11px); paidReactTopUserpic: 42px; paidReactTopNameSkip: 47px; paidReactTopBadgeSkip: 32px; +paidReactCrown: icon{{ "calls/filled_stream_crown-24x24", premiumButtonFg }}; +paidReactCrownTop: 7px; +paidReactCrownSkip: 12px; +paidReactCrownOutline: 1px; +paidReactCrownSmall: icon{{ "calls/filled_stream_crown-20x20", premiumButtonFg }}; +paidReactCrownSmallTop: 4px; +paidReactCrownMargin: margins(0px, 0px, 0px, 0px); paidReactToastLabel: FlatLabel(defaultFlatLabel) { textFg: toastFg; palette: defaultToastPalette;