diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f8e200f160..773ee980ed 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1881,6 +1881,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_settings_title" = "Settings"; "lng_group_call_invite" = "Invite Member"; "lng_group_call_invited_status" = "invited"; +"lng_group_call_muted_by_me_status" = "muted by me"; "lng_group_call_invite_title" = "Invite members"; "lng_group_call_invite_button" = "Invite"; "lng_group_call_add_to_group_one" = "{user} isn't a member of «{group}» yet. Add them to the group?"; diff --git a/Telegram/SourceFiles/calls/calls_group_members.cpp b/Telegram/SourceFiles/calls/calls_group_members.cpp index b39264a63b..61a668b2e4 100644 --- a/Telegram/SourceFiles/calls/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/calls_group_members.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" // Data::CanWriteValue. #include "data/data_session.h" // Data::Session::invitedToCallUsers. #include "settings/settings_common.h" // Settings::CreateButton. +#include "ui/paint/arcs.h" #include "ui/paint/blobs.h" #include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" @@ -46,6 +47,11 @@ constexpr auto kUserpicMinScale = 0.8; constexpr auto kMaxLevel = 1.; constexpr auto kWideScale = 5; +constexpr auto kSpeakerThreshold = { + Group::kDefaultVolume * 0.1f / Group::kMaxVolume, + Group::kDefaultVolume * 0.5f / Group::kMaxVolume, + Group::kDefaultVolume * 1.5f / Group::kMaxVolume }; + auto RowBlobs() -> std::array { return { { { @@ -179,6 +185,30 @@ private: rpl::lifetime lifetime; }; + + struct StatusIcon { + StatusIcon(float volume) + : speaker(st::groupCallMuteCrossLine.icon) + , arcs(std::make_unique( + st::groupCallSpeakerArcsAnimation, + kSpeakerThreshold, + volume, + Ui::Paint::ArcsAnimation::HorizontalDirection::Right)) { + } + const style::icon &speaker; + const std::unique_ptr arcs; + + rpl::lifetime lifetime; + }; + + int statusIconWidth() const; + int statusIconHeight() const; + void paintStatusIcon( + Painter &p, + const style::PeerListItem &st, + const style::font &font, + bool selected); + void refreshStatus() override; void setSounding(bool sounding); void setSpeaking(bool speaking); @@ -194,9 +224,11 @@ private: State _state = State::Inactive; std::unique_ptr _actionRipple; std::unique_ptr _blobsAnimation; + std::unique_ptr _statusIcon; Ui::Animations::Simple _speakingAnimation; // For gray-red/green icon. Ui::Animations::Simple _mutedAnimation; // For gray/red icon. Ui::Animations::Simple _activeAnimation; // For icon cross animation. + Ui::Animations::Simple _arcsAnimation; // For volume arcs animation. uint32 _ssrc = 0; int _volume = Group::kDefaultVolume; bool _sounding = false; @@ -359,6 +391,28 @@ void Row::setSpeaking(bool speaking) { _speaking ? 0. : 1., _speaking ? 1. : 0., st::widgetFadeDuration); + + if (!_speaking || (_state == State::MutedByMe)) { + _statusIcon = nullptr; + } else if (!_statusIcon) { + _statusIcon = std::make_unique( + (float)_volume / Group::kMaxVolume); + + _statusIcon->arcs->startUpdateRequests( + ) | rpl::start_with_next([=] { + auto callback = [=] { + if (_statusIcon) { + _statusIcon->arcs->update(crl::now()); + } + _delegate->rowUpdateRow(this); + }; + _arcsAnimation.start( + std::move(callback), + 0., + 1., + st::groupCallSpeakerArcsAnimation.duration); + }, _statusIcon->lifetime); + } } void Row::setSounding(bool sounding) { @@ -410,6 +464,9 @@ void Row::setSsrc(uint32 ssrc) { void Row::setVolume(int volume) { _volume = volume; + if (_statusIcon) { + _statusIcon->arcs->setValue((float)volume / Group::kMaxVolume); + } } void Row::updateLevel(float level) { @@ -525,6 +582,62 @@ auto Row::generatePaintUserpicCallback() -> PaintRoundImageCallback { }; } +int Row::statusIconWidth() const { + if (!_statusIcon) { + return 0; + } + return _speaking + ? 2 * (_statusIcon->speaker.width() + st::groupCallMenuVolumeSkip) + : 0; +} + +int Row::statusIconHeight() const { + if (!_statusIcon) { + return 0; + } + return _speaking + ? _statusIcon->speaker.height() + : 0; +} + +void Row::paintStatusIcon( + Painter &p, + const style::PeerListItem &st, + const style::font &font, + bool selected) { + if (!_statusIcon) { + return; + } + p.setFont(font); + const auto color = (_speaking + ? st.statusFgActive + : (selected ? st.statusFgOver : st.statusFg))->c; + p.setPen(color); + + const auto speakerRect = QRect( + st.statusPosition + + QPoint(0, (font->height - statusIconHeight()) / 2), + _statusIcon->speaker.size()); + const auto volumeRect = speakerRect.translated( + _statusIcon->speaker.width() + st::groupCallMenuVolumeSkip, + 0); + const auto arcPosition = speakerRect.center() + + QPoint(0, st::groupCallMenuSpeakerArcsSkip); + + const auto volume = std::round(_volume / 100.); + _statusIcon->speaker.paint( + p, + speakerRect.topLeft(), + speakerRect.width(), + color); + p.drawText(volumeRect, QString("%1%").arg(volume), style::al_center); + + p.save(); + p.translate(arcPosition); + _statusIcon->arcs->paint(p, color); + p.restore(); +} + void Row::paintStatusText( Painter &p, const style::PeerListItem &st, @@ -533,6 +646,11 @@ void Row::paintStatusText( int availableWidth, int outerWidth, bool selected) { + p.save(); + const auto &font = st::normalFont; + paintStatusIcon(p, st, font, selected); + p.translate(statusIconWidth(), 0); + const auto guard = gsl::finally([&] { p.restore(); }); if (_state != State::Invited && _state != State::MutedByMe) { PeerListRow::paintStatusText( p, @@ -544,7 +662,7 @@ void Row::paintStatusText( selected); return; } - p.setFont(st::normalFont); + p.setFont(font); if (_state == State::MutedByMe) { p.setPen(st::groupCallMemberMutedIcon); } else { @@ -555,7 +673,7 @@ void Row::paintStatusText( y, outerWidth, (_state == State::MutedByMe - ? "muted by me" + ? tr::lng_group_call_muted_by_me_status(tr::now) : peer()->isSelf() ? tr::lng_status_connecting(tr::now) : tr::lng_group_call_invited_status(tr::now)));