From 34ce5547cbb5f0eb2ea6c44e4aaa5d8899166cfc Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 28 Aug 2026 12:14:11 +0300 Subject: [PATCH] Added rate call screen with stars to call panel. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/animations/call_rate.tgs | Bin 0 -> 11535 bytes Telegram/Resources/icons/calls/call_ended.png | Bin 0 -> 371 bytes .../Resources/icons/calls/call_ended@2x.png | Bin 0 -> 720 bytes .../Resources/icons/calls/call_ended@3x.png | Bin 0 -> 1059 bytes Telegram/Resources/langs/lang.strings | 3 + .../Resources/qrc/telegram/animations.qrc | 1 + Telegram/SourceFiles/calls/calls.style | 40 ++ Telegram/SourceFiles/calls/calls_call.cpp | 108 +++- Telegram/SourceFiles/calls/calls_call.h | 18 + Telegram/SourceFiles/calls/calls_instance.cpp | 3 + Telegram/SourceFiles/calls/calls_panel.cpp | 197 ++++++- Telegram/SourceFiles/calls/calls_panel.h | 13 + .../calls/calls_panel_background.cpp | 13 + .../calls/calls_panel_background.h | 2 + .../SourceFiles/calls/calls_rate_call.cpp | 528 ++++++++++++++++++ Telegram/SourceFiles/calls/calls_rate_call.h | 116 ++++ 17 files changed, 1002 insertions(+), 42 deletions(-) create mode 100644 Telegram/Resources/animations/call_rate.tgs create mode 100644 Telegram/Resources/icons/calls/call_ended.png create mode 100644 Telegram/Resources/icons/calls/call_ended@2x.png create mode 100644 Telegram/Resources/icons/calls/call_ended@3x.png create mode 100644 Telegram/SourceFiles/calls/calls_rate_call.cpp create mode 100644 Telegram/SourceFiles/calls/calls_rate_call.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 84dff065e1..2845c8bd93 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -478,6 +478,8 @@ PRIVATE calls/calls_panel.h calls/calls_panel_background.cpp calls/calls_panel_background.h + calls/calls_rate_call.cpp + calls/calls_rate_call.h calls/calls_signal_bars.cpp calls/calls_signal_bars.h calls/calls_top_bar.cpp diff --git a/Telegram/Resources/animations/call_rate.tgs b/Telegram/Resources/animations/call_rate.tgs new file mode 100644 index 0000000000000000000000000000000000000000..eeffb8baa0fcfc406bdbe52d6ef2ef45c6528c9f GIT binary patch literal 11535 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 RW5NIc002ovPDHLkV1hAlr*;4U literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/calls/call_ended@2x.png b/Telegram/Resources/icons/calls/call_ended@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9caa18a4a491e25e4a933df304d9daaa31cfefe9 GIT binary patch literal 720 zcmV;>0x$iEP)=4whgS$=ObCf)PYBUBsH%E;dWsNQEEYi!{Mrx=!x%Hhpnk_OL2xxg42Q$#=jUya!@(Gf^(vK0uh*N; z=R7g<`MlTbRVo!wP!t8aZP$uB<`hMtlp=)g@9%51+TGoqm|8BEKmXwS{`>noLg?)5 zESt^V+}!-waB^}Y;>Y80qtO_RMp4=QfWl-lnP#&I=~7Bps}*C+vaEbQAKj@Kgphnb zZ&?;&Y_(cZNr-Ne!nk%S+Cd7 z*PiDYh9Q>N!%ewdp3P>Q^DqoWPk{PhFo09Nqq>BU!C)YM0YGXto0ZGuq@M5wMNu5b ziMbJpnx?tC>@a1TrYLR9ljAth;M+a^7mu9tbUK~On_u&SL zd8L%*a=COmo#U62UF5Gw!l^i(Jml5D^51R~)EaO1kJrfDMcL_nyn>$Yu+h7a0|H@J zS6AI`SBTiQt?T-Jrz|RWadAG4bmfnTk1h%NEcozU#;?CR?3+S=O2#>Udp((>~1{QUgX)YSC!G(xCeuYZ1i zRw|VTlTxX4dwW|hm*3yt{}%9nFh&UB`!hE;7Yqh>c6NL|pV#ZP+wCI;X*Qd8cX!2N zF`La^TwJ`qzV-*79K@^~!0-2KnpUk=*>5PNjYgx@YPH*KSot!EcgDI&kBxS_-E1~LO`6SS|A78kxm+$33V{l=qJPd4 z{R_N2wzs#BkB@`FAbf8a1|fto1{vJn-)dxajlw*4Nj)Uhm4v3S0viV+aLd&(F_KPfsr|FMqZ&iK*vwI%BaIEHLkHj4{J7 z7-RW-J`f1F-R{xDy4~(TAdt`Jd64d~x~|7!F{jh{BV~zA;5Zcu1*jHCg45}A7-NY< zqPxozLZFa(kR*u^0@nbHPb3mN2!~4)3I#ZL_;)5&5{X3W^*YG+w*<&Z(==I@6-606 ztfDBgENhz9Z;bCAW2|1UMnuFrv|@s;ZRI?-~OqdwY8jYq%>|)Uh)E zdME|2ud%5H^#^Kv4`=VT?UKKI*#u-M>Fjd*N^x9uNcHNBm?T9Ubv0 zW2l^*oDAtt@Csv$g~Q?TRQ>}m59nI`-azc*$bC&7iVv zHd{KK1}D%S4Z{Gjo12@>&CTzxB23@f+A5Vw1BKG*w9RH4@9FqgZ}6E+CVML6?Cfk| zVZo~R4%}%jmn)mi_6Q}DNuCC>5dIq4-`~H!zJ7Umxw^VKI5@D`Y}UTwAe5pghlht; zNL5vy0J0n&ln~-@IJg{72aIv|<`6=DNNi$4y-Y}Ef../../animations/writing.tgs ../../animations/hours.tgs ../../animations/phone.tgs + ../../animations/call_rate.tgs ../../animations/chat_link.tgs ../../animations/diamond.tgs ../../animations/collectible_username.tgs diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 6240fa71a5..5ff93a9517 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -455,6 +455,46 @@ callRatingComment: InputField(defaultInputField) { } callRatingCommentTop: 8px; +callRateWidth: 300px; +callRateHeight: 152px; +callRateRadius: 28px; +callRateSkip: 16px; +callRateTitleTop: 24px; +callRateDescriptionTop: 50px; +callRateStarsTop: 90px; +callRateStarsHeight: 100px; +callRateStarSize: 37px; +callRateStarSkip: 4px; +callRateBurstSize: 133px; +callRateShowShift: 24px; +callRateStarShowShift: 30px; +callRateTitle: FlatLabel(defaultFlatLabel) { + minWidth: 40px; + textFg: callNameFg; + align: align(top); + style: TextStyle(defaultTextStyle) { + font: font(15px semibold); + } +} +callRateDescription: FlatLabel(callRateTitle) { + style: TextStyle(defaultTextStyle) { + font: font(15px); + } +} +callRateStarRipple: RippleAnimation(defaultRippleAnimation) { + color: callMuteRipple; +} +callRateEndedIcon: icon {{ "calls/call_ended", callNameFg }}; +callRateEndedSkip: 4px; +callRateCloseWidth: 358px; +callRateCloseHeight: 52px; +callRateCloseRadius: 8px; +callRateCloseSkip: 24px; +callRateCloseFont: font(18px semibold); +callRateCloseRipple: RippleAnimation(defaultRippleAnimation) { + color: callIconActiveRipple; +} + callDebugPadding: margins(24px, 0px, 24px, 0px); callDebugLabel: FlatLabel(defaultFlatLabel) { margin: callDebugPadding; diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 4edd70beda..a67723f582 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -622,7 +622,78 @@ crl::time Call::getDurationMs() const { return _startTime ? (crl::now() - _startTime) : 0; } +void Call::takeRatingToPanel() { + Expects(_ratingRequested); + + _ratingInPanel = true; +} + +void Call::setRating(int rating) { + _rating = rating; +} + +void Call::finishRating() { + if (!_ratingInPanel) { + return; + } + _ratingInPanel = false; + _ratingRequested = false; + if (_rating > 0 && _id && _accessHash) { + const auto session = &_user->session(); + session->api().request(MTPphone_SetCallRating( + MTP_flags(0), + MTP_inputPhoneCall( + MTP_long(_id), + MTP_long(_accessHash)), + MTP_int(_rating), + MTP_string() + )).done([=](const MTPUpdates &updates) { + session->api().applyUpdates(updates); + }).send(); + } + _delegate->callFinished(this); +} + +void Call::showRatingBox() { + Expects(_ratingRequested); + + _ratingRequested = false; + + const auto window = Core::App().windowFor(::Window::SeparateId(_user)); + const auto session = &_user->session(); + const auto callId = _id; + const auto callAccessHash = _accessHash; + auto owned = Box(Core::App().settings().sendSubmitWay()); + const auto box = window + ? window->show(std::move(owned)) + : Ui::show(std::move(owned)); + const auto sender = box->lifetime().make_state( + &session->mtp()); + box->sends( + ) | rpl::take( + 1 // Instead of keeping requestId. + ) | rpl::on_next([=](const Ui::RateCallBox::Result &r) { + sender->request(MTPphone_SetCallRating( + MTP_flags(0), + MTP_inputPhoneCall( + MTP_long(callId), + MTP_long(callAccessHash)), + MTP_int(r.rating), + MTP_string(r.comment) + )).done([=](const MTPUpdates &updates) { + session->api().applyUpdates(updates); + box->closeBox(); + }).fail([=] { + box->closeBox(); + }).send(); + }, box->lifetime()); +} + void Call::hangup(Data::GroupCall *migrateCall, const QString &migrateSlug) { + if (_ratingInPanel) { + finishRating(); + return; + } const auto state = _state.current(); if (state == State::Busy || state == State::MigrationHangingUp) { @@ -807,36 +878,10 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { } } if (data.is_need_rating() && _id && _accessHash) { - const auto window = Core::App().windowFor( - ::Window::SeparateId(_user)); - const auto session = &_user->session(); - const auto callId = _id; - const auto callAccessHash = _accessHash; - auto owned = Box( - Core::App().settings().sendSubmitWay()); - const auto box = window - ? window->show(std::move(owned)) - : Ui::show(std::move(owned)); - const auto sender = box->lifetime().make_state( - &session->mtp()); - box->sends( - ) | rpl::take( - 1 // Instead of keeping requestId. - ) | rpl::on_next([=](const Ui::RateCallBox::Result &r) { - sender->request(MTPphone_SetCallRating( - MTP_flags(0), - MTP_inputPhoneCall( - MTP_long(callId), - MTP_long(callAccessHash)), - MTP_int(r.rating), - MTP_string(r.comment) - )).done([=](const MTPUpdates &updates) { - session->api().applyUpdates(updates); - box->closeBox(); - }).fail([=] { - box->closeBox(); - }).send(); - }, box->lifetime()); + _ratingRequested = true; + } + if (const auto duration = data.vduration()) { + _discardedDuration = duration->v; } const auto reason = data.vreason(); if (reason @@ -854,6 +899,9 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { } else { setState(State::EndedByOtherDevice); } + if (_ratingRequested && !_ratingInPanel) { + showRatingBox(); + } } return true; case mtpc_phoneCallAccepted: { diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index d40d0794c4..73baeb3b50 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -228,6 +228,19 @@ public: crl::time getDurationMs() const; float64 getWaitingSoundPeakValue() const; + [[nodiscard]] bool ratingRequested() const { + return _ratingRequested; + } + [[nodiscard]] bool ratingInPanel() const { + return _ratingInPanel; + } + [[nodiscard]] int discardedDuration() const { + return _discardedDuration; + } + void takeRatingToPanel(); + void setRating(int rating); + void finishRating(); + void applyUserConfirmation(); void answer(); void hangup( @@ -290,6 +303,7 @@ private: = MTP_phoneCallDiscardReasonDisconnect(), Data::GroupCall *migrateCall = nullptr); void finishByMigration(const QString &slug); + void showRatingBox(); void startOutgoing(); void startIncoming(); void startWaitingTrack(); @@ -362,6 +376,10 @@ private: CallId _id = 0; uint64 _accessHash = 0; + int _rating = 0; + int _discardedDuration = 0; + bool _ratingRequested = false; + bool _ratingInPanel = false; uint64 _keyFingerprint = 0; CallId _conferenceId = 0; diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 9af91fa6ea..581b4f10c9 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -99,6 +99,9 @@ DhConfig Instance::Delegate::getDhConfig() const { void Instance::Delegate::callFinished(not_null call) { crl::on_main(call, [=] { + if (call->ratingInPanel()) { + return; + } _instance->destroyCall(call); }); } diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index b61d66114b..b5b1388be4 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge #include "calls/calls_panel_background.h" +#include "calls/calls_rate_call.h" #include "data/data_photo.h" #include "data/data_session.h" #include "data/data_user.h" @@ -79,6 +80,7 @@ namespace { constexpr auto kHideControlsTimeout = 5 * crl::time(1000); constexpr auto kHideControlsQuickTimeout = 2 * crl::time(1000); +constexpr auto kNameFadeDuration = crl::time(70); [[nodiscard]] QByteArray BatterySvg( const QSize &s, @@ -332,6 +334,8 @@ void Panel::initWindow() { _answerHangupRedial->y(), buttonsWidth, _answerHangupRedial->height()).contains(widgetPoint) + || (_rateCall && _rateCall->geometry().contains(widgetPoint)) + || (_rateClose && _rateClose->geometry().contains(widgetPoint)) || (!_outgoingPreviewInBody && _outgoingVideoBubble->geometry().contains(widgetPoint)); if (inControls) { @@ -651,6 +655,16 @@ void Panel::refreshIncomingGeometry() { void Panel::reinitWithCall(Call *call) { _callLifetime.destroy(); + _rateNameFade.stop(); + _rateCall = nullptr; + _rateClose = nullptr; + _rateNameSwapped = false; + _endedDuration = 0; + _rateEndedIconPosition = QPoint(); + _name->setOpacity(1.); + _bodySt = _outgoingPreviewInBody + ? &st::callBodyWithPreview + : &st::callBodyLayout; _call = call; const auto guard = gsl::finally([&] { updateControlsShown(); @@ -1017,10 +1031,14 @@ void Panel::showControls() { widget()->showChildren(); _pinOnTop->setVisible(!_fullScreenOrMaximized.current()); - _decline->setVisible(_decline->toggled()); - _cancel->setVisible(_cancel->toggled()); - _screencast->setVisible(_screencast->toggled()); - _addPeople->setVisible(_addPeople->toggled()); + if (_rateCall) { + hideControlsForRating(); + } else { + _decline->setVisible(_decline->toggled()); + _cancel->setVisible(_cancel->toggled()); + _screencast->setVisible(_screencast->toggled()); + _addPeople->setVisible(_addPeople->toggled()); + } const auto shown = !_incomingFrameSize.isEmpty(); _incoming->widget()->setVisible(shown); @@ -1129,6 +1147,97 @@ void Panel::refreshOutgoingPreviewInBody(State state) { updateControlsGeometry(); } +void Panel::showRateCall() { + Expects(_call != nullptr); + Expects(_rateCall == nullptr); + + _call->takeRatingToPanel(); + const auto local = int(_call->getDurationMs() / 1000); + _endedDuration = local ? local : _call->discardedDuration(); + _bodySt = &st::callBodyWithPreview; + + _rateCall = base::make_unique_q(widget()); + _rateCall->ratingValue( + ) | rpl::on_next([=](int rating) { + if (_call) { + _call->setRating(rating); + } + }, _rateCall->lifetime()); + _rateClose = base::make_unique_q(widget()); + + hideControlsForRating(); + updateControlsGeometry(); + updateTextColors(); + updateStatusText(_call->state()); + + _rateCall->show(); + _rateCall->showAnimated(); + _rateClose->switchToClose(hangupCircleRect(), [=] { finishRateCall(); }); + + _rateNameFade.start([=] { + const auto value = _rateNameFade.value(1.); + if (!_rateNameSwapped && value >= 0.5) { + _rateNameSwapped = true; + _name->setText(tr::lng_call_ended_title(tr::now)); + updateControlsGeometry(); + } + _name->setOpacity(std::abs(2. * value - 1.)); + }, 0., 1., 2 * kNameFadeDuration); + + widget()->update(); +} + +void Panel::finishRateCall() { + if (_call) { + _call->finishRating(); + } +} + +void Panel::hideControlsForRating() { + _answerHangupRedial->hide(); + _camera->hide(); + if (_startVideo) { + _startVideo->hide(); + } + _decline->hide(anim::type::instant); + _cancel->hide(anim::type::instant); + _screencast->hide(anim::type::instant); + _mute->hide(anim::type::instant); + _addPeople->hide(anim::type::instant); + if (_fingerprint) { + _fingerprint->hide(); + } + if (_remoteAudioMute) { + _remoteAudioMute->hide(); + } + if (_remoteLowBattery) { + _remoteLowBattery->hide(); + } + hideButtonTooltip(); +} + +QRect Panel::hangupCircleRect() const { + const auto &st = st::callHangup; + return QRect( + _answerHangupRedial->x() + st.bgPosition.x(), + _answerHangupRedial->y() + st.bgPosition.y(), + st.bgSize, + st.bgSize); +} + +QRect Panel::rateCloseRect() const { + const auto circle = hangupCircleRect(); + const auto width = std::min( + st::callRateCloseWidth, + widget()->width() - 2 * st::callRateCloseSkip); + const auto height = st::callRateCloseHeight; + return QRect( + (widget()->width() - width) / 2, + circle.y() + (circle.height() - height) / 2, + width, + height); +} + void Panel::createPinOnTop() { const auto wrap = _window->controlsWrap(); _pinOnTop = base::make_unique_q( @@ -1225,6 +1334,7 @@ void Panel::updateControlsShown() { const auto shown = !_incoming || _incoming->widget()->isHidden() || _controlsShownForce + || _rateCall || !_hideControlsRequested; if (_controlsShown != shown) { _controlsShown = shown; @@ -1322,7 +1432,11 @@ void Panel::updateControlsGeometry() { st::callOutgoingPreviewMax.height(), bodyPreviewHeightMax, })); + const auto rateHeight = _rateCall + ? (st::callRateSkip + st::callRateHeight) + : 0; const auto contentHeight = bodyContentHeight + + rateHeight + (_outgoingPreviewInBody ? bodyPreviewSize.height() : 0); const auto remainingHeight = std::max(available - contentHeight, 0); const auto skipHeight = remainingHeight @@ -1351,6 +1465,14 @@ void Panel::updateControlsGeometry() { _bodyTop + _bodySt->nameTop); updateStatusGeometry(); + if (_rateCall) { + _rateCall->setGeometry( + (widget()->width() - st::callRateWidth) / 2, + _bodyTop + bodyContentHeight + st::callRateSkip, + st::callRateWidth, + RateCall::Height()); + } + if (_remoteAudioMute) { _remoteAudioMute->moveToLeft( (widget()->width() - _remoteAudioMute->width()) / 2, @@ -1383,6 +1505,9 @@ void Panel::updateControlsGeometry() { } updateHangupGeometry(); + if (_rateClose) { + _rateClose->setGeometry(rateCloseRect()); + } updateButtonTooltipGeometry(); } @@ -1430,11 +1555,19 @@ void Panel::updateStatusGeometry() { if (widget()->size().isEmpty()) { return; } + const auto &icon = st::callRateEndedIcon; + const auto showIcon = (_rateCall && _endedDuration > 0); + const auto skip = showIcon + ? (icon.width() + st::callRateEndedSkip) + : 0; _status->resizeToNaturalWidth( - widget()->width() - 2 * st::callInnerPadding); - _status->moveToLeft( - (widget()->width() - _status->width()) / 2, - _bodyTop + _bodySt->statusTop); + widget()->width() - 2 * st::callInnerPadding - skip); + const auto top = _bodyTop + _bodySt->statusTop; + const auto left = (widget()->width() - _status->width() - skip) / 2; + _status->moveToLeft(left + skip, top); + _rateEndedIconPosition = showIcon + ? QPoint(left, top + (_status->height() - icon.height()) / 2) + : QPoint(); } auto Panel::bottomButtons() const @@ -1585,6 +1718,18 @@ void Panel::paint(QRect clip) { } } + if (_rateCall && !_rateEndedIconPosition.isNull()) { + const auto fg = st::callName.textFg; + const auto color = (_background + ? _background->textColorOverride(fg) + : std::optional()).value_or(fg->c); + st::callRateEndedIcon.paint( + p, + _rateEndedIconPosition, + widget()->width(), + color); + } + if (_incoming && _incoming->widget()->isHidden()) { _call->videoIncoming()->markFrameShown(); } @@ -1592,7 +1737,9 @@ void Panel::paint(QRect clip) { bool Panel::handleClose() const { if (_call) { - if (_call->state() == Call::State::WaitingUserConfirmation + if (_call->ratingInPanel()) { + _call->finishRating(); + } else if (_call->state() == Call::State::WaitingUserConfirmation || _call->state() == Call::State::Busy || _call->state() == Call::State::Starting || _call->state() == Call::State::WaitingIncoming) { @@ -1631,6 +1778,13 @@ void Panel::stateChanged(State state) { const auto isWaitingUser = (state == State::WaitingUserConfirmation); _window->togglePowerSaveBlocker(!isBusy && !isWaitingUser); + if (!_rateCall + && _call->ratingRequested() + && !window()->isHidden() + && (state == State::Ended || state == State::EndedByOtherDevice)) { + showRateCall(); + } + if ((state != State::HangingUp) && (state != State::MigrationHangingUp) && (state != State::Ended) @@ -1720,6 +1874,9 @@ void Panel::refreshAnswerHangupRedialLabel() { void Panel::updateStatusText(State state) { auto statusText = [this, state]() -> QString { + if (_rateCall && _endedDuration > 0) { + return Ui::FormatDurationText(_endedDuration); + } switch (state) { case State::Starting: case State::WaitingInit: @@ -1757,15 +1914,31 @@ void Panel::updateStatusText(State state) { } void Panel::updateTextColors() { + const auto statusFg = _rateCall + ? st::callName.textFg + : st::callStatus.textFg; if (!_background) { _name->setTextColorOverride(std::nullopt); - _status->setTextColorOverride(std::nullopt); + _status->setTextColorOverride(_rateCall + ? std::make_optional(statusFg->c) + : std::nullopt); + if (_rateCall) { + _rateCall->setTextColorOverride(std::nullopt); + } return; } _name->setTextColorOverride( _background->textColorOverride(st::callName.textFg)); - _status->setTextColorOverride( - _background->textColorOverride(st::callStatus.textFg)); + auto status = _background->textColorOverride(statusFg); + if (_rateCall && !status) { + status = statusFg->c; + } + _status->setTextColorOverride(status); + if (_rateCall) { + _rateCall->setTextColorOverride( + _background->textColorOverride(st::callRateTitle.textFg)); + _rateCall->setCardOverlay(_background->cardOverlay()); + } } void Panel::startDurationUpdateTimer(crl::time currentDuration) { diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index 7ccd480d9f..01ff853a57 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -67,6 +67,8 @@ class Userpic; class SignalBars; class VideoBubble; class PanelBackground; +class RateCall; +class EndCloseButton; struct DeviceSelection; struct ConferencePanelMigration; @@ -158,6 +160,11 @@ private: void refreshIncomingGeometry(); void refreshOutgoingPreviewInBody(State state); + void showRateCall(); + void finishRateCall(); + void hideControlsForRating(); + [[nodiscard]] QRect hangupCircleRect() const; + [[nodiscard]] QRect rateCloseRect() const; void createPinOnTop(); [[nodiscard]] QRect pinOnTopRect() const; void toggleFullScreen(bool fullscreen); @@ -206,6 +213,12 @@ private: bool _buttonLabelsShown = true; base::unique_qptr _name; base::unique_qptr _status; + base::unique_qptr _rateCall; + base::unique_qptr _rateClose; + Ui::Animations::Simple _rateNameFade; + QPoint _rateEndedIconPosition; + int _endedDuration = 0; + bool _rateNameSwapped = false; base::unique_qptr _conferenceParticipants; base::unique_qptr _fingerprint; base::unique_qptr> _remoteAudioMute; diff --git a/Telegram/SourceFiles/calls/calls_panel_background.cpp b/Telegram/SourceFiles/calls/calls_panel_background.cpp index 2c374850f8..b8cfbf3892 100644 --- a/Telegram/SourceFiles/calls/calls_panel_background.cpp +++ b/Telegram/SourceFiles/calls/calls_panel_background.cpp @@ -20,6 +20,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/top_background_gradient.h" namespace Calls { +namespace { + +constexpr auto kDarkOverlayOpacity = 0.14; +constexpr auto kDarkenAboveLightness = 0.3; + +} // namespace PanelBackground::PanelBackground( not_null peer, @@ -217,6 +223,13 @@ std::optional PanelBackground::textColorOverride( return std::nullopt; } +QColor PanelBackground::cardOverlay() const { + const auto base = edgeColor().value_or(st::callBgOpaque->c); + return (base.lightnessF() > kDarkenAboveLightness) + ? QColor(0, 0, 0, anim::interpolate(0, 255, kDarkOverlayOpacity)) + : st::callIconBg->c; +} + rpl::lifetime &PanelBackground::lifetime() { return _lifetime; } diff --git a/Telegram/SourceFiles/calls/calls_panel_background.h b/Telegram/SourceFiles/calls/calls_panel_background.h index e4c00770ea..91937e5ee1 100644 --- a/Telegram/SourceFiles/calls/calls_panel_background.h +++ b/Telegram/SourceFiles/calls/calls_panel_background.h @@ -42,6 +42,8 @@ public: [[nodiscard]] std::optional textColorOverride( const style::color &defaultColor) const; + [[nodiscard]] QColor cardOverlay() const; + private: void updateBrush( QSize widgetSize, diff --git a/Telegram/SourceFiles/calls/calls_rate_call.cpp b/Telegram/SourceFiles/calls/calls_rate_call.cpp new file mode 100644 index 0000000000..e60bcfe7af --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_rate_call.cpp @@ -0,0 +1,528 @@ +/* +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/calls_rate_call.h" + +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "styles/style_calls.h" + +#include + +namespace Calls { +namespace { + +constexpr auto kBurstMinRating = 4; +constexpr auto kShowDuration = crl::time(250); +constexpr auto kStarShowDelay = crl::time(16); +constexpr auto kSelectDuration = crl::time(250); +constexpr auto kCardShowScaleFrom = 0.7; +constexpr auto kStarShowScaleFrom = 0.3; +constexpr auto kStarPressedScale = 0.8; +constexpr auto kDarkOpacity = 0.14; +constexpr auto kRippleOpacity = 0.3; +constexpr auto kPathSize = 96.; +constexpr auto kPathStroke = 6.; +constexpr auto kPathMiterLimit = 4.; +constexpr auto kCloseTextShadeOpacity = 0.15; +constexpr auto kCloseRippleOpacity = 0.12; +constexpr auto kMaxBursts = 5; + +struct StarPoint { + float64 x = 0.; + float64 y = 0.; + float64 inX = 0.; + float64 inY = 0.; + float64 outX = 0.; + float64 outY = 0.; +}; + +const StarPoint kStarPath[] = { + { 46.080, 69.940, 0.855, -0.588, 0.000, 0.000 }, + { 32.727, 79.118, 0.000, 0.000, -2.274, 1.563 }, + { 25.770, 77.844, 1.568, 2.267, -0.860, -1.243 }, + { 25.094, 73.591, -0.432, 1.447, 0.000, 0.000 }, + { 29.725, 58.097, 0.000, 0.000, 0.296, -0.992 }, + { 28.848, 55.405, 0.825, 0.629, 0.000, 0.000 }, + { 15.963, 45.583, 0.000, 0.000, -2.194, -1.673 }, + { 15.029, 38.595, -1.678, 2.187, 0.920, -1.199 }, + { 18.879, 36.640, -1.514, 0.037, 0.000, 0.000 }, + { 35.095, 36.242, 0.000, 0.000, 1.038, -0.025 }, + { 37.392, 34.578, -0.345, 0.976, 0.000, 0.000 }, + { 42.783, 19.330, 0.000, 0.000, 0.918, -2.597 }, + { 49.162, 16.285, -2.605, -0.915, 1.429, 0.502 }, + { 52.218, 19.330, -0.503, -1.424, 0.000, 0.000 }, + { 57.608, 34.578, 0.000, 0.000, 0.345, 0.976 }, + { 59.905, 36.242, -1.038, -0.025, 0.000, 0.000 }, + { 76.121, 36.640, 0.000, 0.000, 2.762, 0.068 }, + { 80.998, 41.746, 0.068, -2.752, -0.037, 1.509 }, + { 79.037, 45.583, 1.203, -0.917, 0.000, 0.000 }, + { 66.152, 55.405, 0.000, 0.000, -0.825, 0.629 }, + { 65.275, 58.097, -0.296, -0.992, 0.000, 0.000 }, + { 69.906, 73.591, 0.000, 0.000, 0.789, 2.639 }, + { 66.541, 79.792, 2.647, -0.786, -1.452, 0.431 }, + { 62.273, 79.118, 1.247, 0.857, 0.000, 0.000 }, + { 48.920, 69.940, 0.000, 0.000, -0.855, -0.588 }, +}; + +[[nodiscard]] QPainterPath StarPath(int size) { + const auto scale = size / kPathSize; + const auto map = [&](float64 x, float64 y) { + return QPointF(x * scale, y * scale); + }; + const auto count = int(std::size(kStarPath)); + auto result = QPainterPath(); + result.moveTo(map(kStarPath[0].x, kStarPath[0].y)); + for (auto i = 0; i != count; ++i) { + const auto &from = kStarPath[i]; + const auto &to = kStarPath[(i + 1) % count]; + result.cubicTo( + map(from.x + from.outX, from.y + from.outY), + map(to.x + to.inX, to.y + to.inY), + map(to.x, to.y)); + } + result.closeSubpath(); + return result; +} + +[[nodiscard]] QImage StarImage(int size, QColor color, bool filled) { + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + result.fill(Qt::transparent); + + auto p = QPainter(&result); + auto hq = PainterHighQualityEnabler(p); + const auto path = StarPath(size); + p.setPen(Qt::NoPen); + p.setBrush(color); + if (filled) { + p.drawPath(path); + } else { + auto stroker = QPainterPathStroker(); + stroker.setWidth(kPathStroke * size / kPathSize); + stroker.setJoinStyle(Qt::MiterJoin); + stroker.setMiterLimit(kPathMiterLimit); + p.drawPath(stroker.createStroke(path).intersected(path)); + } + p.end(); + + return result; +} + +} // namespace + +RateCall::RateCall(QWidget *parent) +: RpWidget(parent) +, _title(st::callRateTitle.minWidth) +, _description(st::callRateDescription.minWidth) { + _title.setText(st::callRateTitle.style, tr::lng_call_rate_title(tr::now)); + _description.setText( + st::callRateDescription.style, + tr::lng_call_rate_description(tr::now)); + _cardOverlay = QColor(0, 0, 0, anim::interpolate(0, 255, kDarkOpacity)); + setMouseTracking(true); + resize(st::callRateWidth, Height()); +} + +RateCall::~RateCall() = default; + +int RateCall::Height() { + return st::callRateStarsTop + st::callRateStarsHeight; +} + +void RateCall::showAnimated() { + _showAnimation.stop(); + _showAnimation = {}; + _showAnimation.start( + [=] { update(); }, + 0., + 1., + kShowDuration + (kStarsCount - 1) * kStarShowDelay, + anim::linear); + update(); +} + +void RateCall::setCardOverlay(QColor color) { + if (_cardOverlay == color) { + return; + } + _cardOverlay = color; + update(); +} + +void RateCall::setTextColorOverride(std::optional color) { + if (_textColorOverride == color) { + return; + } + _textColorOverride = color; + _starOutline = _starFilled = QImage(); + update(); +} + +int RateCall::rating() const { + return _rating.current(); +} + +rpl::producer RateCall::ratingValue() const { + return _rating.value(); +} + +QColor RateCall::textColor() const { + return _textColorOverride.value_or(st::callRateTitle.textFg->c); +} + +QRect RateCall::cardRect() const { + return QRect( + (width() - st::callRateWidth) / 2, + 0, + st::callRateWidth, + st::callRateHeight); +} + +QRect RateCall::starRect(int index) const { + const auto single = st::callRateStarSize; + const auto full = kStarsCount * single + + (kStarsCount - 1) * st::callRateStarSkip; + const auto left = (width() - full) / 2 + + index * (single + st::callRateStarSkip); + return QRect(left, st::callRateStarsTop, single, single); +} + +int RateCall::starByPosition(QPoint position) const { + for (auto i = 0; i != kStarsCount; ++i) { + if (starRect(i).contains(position)) { + return i; + } + } + return -1; +} + +float64 RateCall::cardShown() const { + const auto total = kShowDuration + (kStarsCount - 1) * kStarShowDelay; + const auto passed = _showAnimation.value(1.) * total; + return anim::sineInOut(1., std::clamp(passed / kShowDuration, 0., 1.)); +} + +float64 RateCall::starShown(int index) const { + const auto total = kShowDuration + (kStarsCount - 1) * kStarShowDelay; + const auto passed = _showAnimation.value(1.) * total + - index * kStarShowDelay; + return anim::sineInOut(1., std::clamp(passed / kShowDuration, 0., 1.)); +} + +const QImage &RateCall::starImage(bool filled) const { + const auto color = textColor(); + if (_starColor != color) { + _starColor = color; + _starOutline = _starFilled = QImage(); + } + auto &image = filled ? _starFilled : _starOutline; + if (image.isNull()) { + image = StarImage(st::callRateStarSize, color, filled); + } + return image; +} + +void RateCall::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + auto hq = PainterHighQualityEnabler(p); + + paintCard(p, cardShown()); + for (auto i = 0; i != kStarsCount; ++i) { + paintStar(p, i, starShown(i)); + } + paintBurst(p); +} + +void RateCall::paintCard(QPainter &p, float64 shown) { + if (shown <= 0.) { + return; + } + const auto card = cardRect(); + const auto scale = kCardShowScaleFrom + + (1. - kCardShowScaleFrom) * shown; + + p.save(); + p.setOpacity(shown); + p.translate(0, anim::interpolate(st::callRateShowShift, 0, shown)); + p.translate(card.center()); + p.scale(scale, scale); + p.translate(-card.center()); + + p.setPen(Qt::NoPen); + p.setBrush(_cardOverlay); + p.drawRoundedRect(card, st::callRateRadius, st::callRateRadius); + + p.setPen(textColor()); + _title.draw(p, { + .position = QPoint(card.x(), card.y() + st::callRateTitleTop), + .outerWidth = width(), + .availableWidth = card.width(), + .align = style::al_top, + }); + _description.draw(p, { + .position = QPoint(card.x(), card.y() + st::callRateDescriptionTop), + .outerWidth = width(), + .availableWidth = card.width(), + .align = style::al_top, + }); + p.restore(); +} + +void RateCall::paintStar(QPainter &p, int index, float64 shown) { + if (shown <= 0.) { + return; + } + auto &star = _stars[index]; + const auto rect = starRect(index); + if (star.ripple && !star.ripple->empty()) { + auto color = textColor(); + color.setAlphaF(color.alphaF() * kRippleOpacity); + p.setOpacity(shown); + star.ripple->paint(p, rect.x(), rect.y(), width(), &color); + p.setOpacity(1.); + } + const auto filled = star.filled.value(star.filledTo); + const auto scale = star.pressed.value(star.pressedTo) + * (kStarShowScaleFrom + (1. - kStarShowScaleFrom) * shown); + + p.save(); + p.translate(0, anim::interpolate(st::callRateStarShowShift, 0, shown)); + p.translate(rect.center()); + p.scale(scale, scale); + p.translate(-rect.center()); + if (filled < 1.) { + p.setOpacity(shown * (1. - filled)); + p.drawImage(rect.topLeft(), starImage(false)); + } + if (filled > 0.) { + p.setOpacity(shown * filled); + p.drawImage(rect.topLeft(), starImage(true)); + } + p.restore(); + p.setOpacity(1.); +} + +void RateCall::paintBurst(QPainter &p) { + const auto size = st::callRateBurstSize; + for (const auto &burst : _bursts) { + if (!burst.icon || !burst.icon->animating()) { + continue; + } + burst.icon->paint( + p, + burst.center.x() - size / 2, + burst.center.y() - size / 2); + } +} + +void RateCall::setOver(int index) { + if (_over == index) { + return; + } + _over = index; + setCursor((_over >= 0) ? style::cur_pointer : style::cur_default); +} + +void RateCall::applyPreview(int preview) { + if (_preview == preview) { + return; + } + _preview = preview; + const auto shown = _preview ? _preview : _rating.current(); + for (auto i = 0; i != kStarsCount; ++i) { + auto &star = _stars[i]; + const auto filled = (i < shown) ? 1. : 0.; + const auto pressed = (_preview && (i < _preview)) + ? kStarPressedScale + : 1.; + if (star.filledTo != filled) { + star.filled.start( + [=] { update(); }, + star.filled.value(star.filledTo), + filled, + kSelectDuration); + star.filledTo = filled; + } + if (star.pressedTo != pressed) { + star.pressed.start( + [=] { update(); }, + star.pressed.value(star.pressedTo), + pressed, + kSelectDuration); + star.pressedTo = pressed; + } + } + update(); +} + +void RateCall::commit(int index) { + _rating = index + 1; + applyPreview(0); + if (_rating.current() >= kBurstMinRating) { + startBurst(index); + } +} + +void RateCall::startBurst(int index) { + const auto free = ranges::find_if(_bursts, [](const Burst &burst) { + return !burst.icon || !burst.icon->animating(); + }); + const auto burst = (free != end(_bursts)) + ? &*free + : (int(_bursts.size()) < kMaxBursts) + ? &_bursts.emplace_back() + : &*ranges::min_element(_bursts, ranges::less(), &Burst::started); + if (!burst->icon) { + burst->icon = Lottie::MakeIcon({ + .name = u"call_rate"_q, + .sizeOverride = QSize( + st::callRateBurstSize, + st::callRateBurstSize), + }); + if (!burst->icon->valid()) { + burst->icon = nullptr; + return; + } + } + burst->center = starRect(index).center(); + burst->started = crl::now(); + const auto last = burst->icon->framesCount() - 1; + burst->icon->animate([=] { update(); }, 0, last); + update(); +} + +void RateCall::mouseMoveEvent(QMouseEvent *e) { + setOver(starByPosition(e->pos())); +} + +void RateCall::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) { + return; + } + setOver(starByPosition(e->pos())); + if (_over < 0) { + return; + } + _pressed = _over; + auto &star = _stars[_pressed]; + const auto rect = starRect(_pressed); + if (!star.ripple) { + star.ripple = std::make_unique( + st::callRateStarRipple, + Ui::RippleAnimation::EllipseMask(rect.size()), + [=] { update(); }); + } + star.ripple->add(e->pos() - rect.topLeft()); + applyPreview(_pressed + 1); +} + +void RateCall::mouseReleaseEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) { + return; + } + const auto pressed = std::exchange(_pressed, -1); + if (pressed < 0) { + return; + } else if (const auto &ripple = _stars[pressed].ripple) { + ripple->lastStop(); + } + setOver(starByPosition(e->pos())); + if (_over == pressed) { + commit(pressed); + } else { + applyPreview(0); + } +} + +void RateCall::leaveEventHook(QEvent *e) { + setOver(-1); +} + +EndCloseButton::EndCloseButton(QWidget *parent) +: RippleButton(parent, st::callRateCloseRipple) +, _text(tr::lng_close(tr::now)) { + setDisabled(true); + hide(); +} + +void EndCloseButton::switchToClose(QRect hangupCircle, Fn close) { + _from = hangupCircle; + setClickedCallback(std::move(close)); + _animation.start([=] { update(); }, 0., 1., st::callPanelDuration); + _animation.setFinishedCallback([=] { setDisabled(false); }); + show(); + update(); +} + +void EndCloseButton::prepareFrame() { + const auto ratio = style::DevicePixelRatio(); + if (_frame.size() != size() * ratio) { + _frame = QImage(size() * ratio, QImage::Format_ARGB32_Premultiplied); + _frame.setDevicePixelRatio(ratio); + } + _frame.fill(Qt::transparent); + + auto p = QPainter(&_frame); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::callIconBgActive); + p.drawRoundedRect( + rect(), + st::callRateCloseRadius, + st::callRateCloseRadius); + + auto ripple = st::callIconFgActive->c; + ripple.setAlphaF(ripple.alphaF() * kCloseRippleOpacity); + paintRipple(p, 0, 0, &ripple); + + p.setFont(st::callRateCloseFont); + p.setCompositionMode(QPainter::CompositionMode_DestinationOut); + p.setPen(QColor(0, 0, 0, 255)); + p.drawText(rect(), Qt::AlignCenter, _text); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + auto shade = st::callIconFgActive->c; + shade.setAlphaF(shade.alphaF() * kCloseTextShadeOpacity); + p.setPen(shade); + p.drawText(rect(), Qt::AlignCenter, _text); +} + +void EndCloseButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto shown = _animation.value(1.); + if (shown < 1.) { + auto hq = PainterHighQualityEnabler(p); + const auto circle = _from.translated(-pos()); + p.setOpacity(1. - shown); + p.setPen(Qt::NoPen); + p.setBrush(st::callHangupBg); + p.drawEllipse(circle); + st::callHangup.button.icon.paintInCenter( + p, + circle, + st::callIconFg->c); + } + + prepareFrame(); + p.setOpacity(shown); + p.drawImage(0, 0, _frame); +} + +QImage EndCloseButton::prepareRippleMask() const { + return Ui::RippleAnimation::RoundRectMask( + size(), + st::callRateCloseRadius); +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_rate_call.h b/Telegram/SourceFiles/calls/calls_rate_call.h new file mode 100644 index 0000000000..a13b57bb12 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_rate_call.h @@ -0,0 +1,116 @@ +/* +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 +*/ +#pragma once + +#include "ui/effects/animations.h" +#include "ui/rp_widget.h" +#include "ui/text/text.h" +#include "ui/widgets/buttons.h" + +namespace Lottie { +class Icon; +} // namespace Lottie + +namespace Ui { +class RippleAnimation; +} // namespace Ui + +namespace Calls { + +class RateCall final : public Ui::RpWidget { +public: + explicit RateCall(QWidget *parent); + ~RateCall(); + + void showAnimated(); + void setTextColorOverride(std::optional color); + void setCardOverlay(QColor color); + + [[nodiscard]] int rating() const; + [[nodiscard]] rpl::producer ratingValue() const; + + [[nodiscard]] static int Height(); + +private: + static constexpr auto kStarsCount = 5; + + struct Burst { + std::unique_ptr icon; + QPoint center; + crl::time started = 0; + }; + + struct Star { + std::unique_ptr ripple; + Ui::Animations::Simple filled; + Ui::Animations::Simple pressed; + float64 filledTo = 0.; + float64 pressedTo = 1.; + }; + + void paintEvent(QPaintEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void leaveEventHook(QEvent *e) override; + + void paintCard(QPainter &p, float64 shown); + void paintStar(QPainter &p, int index, float64 shown); + void paintBurst(QPainter &p); + + void setOver(int index); + void applyPreview(int preview); + void commit(int index); + void startBurst(int index); + + [[nodiscard]] QRect cardRect() const; + [[nodiscard]] QRect starRect(int index) const; + [[nodiscard]] int starByPosition(QPoint position) const; + [[nodiscard]] QColor textColor() const; + [[nodiscard]] float64 cardShown() const; + [[nodiscard]] float64 starShown(int index) const; + [[nodiscard]] const QImage &starImage(bool filled) const; + + Ui::Text::String _title; + Ui::Text::String _description; + std::array _stars; + std::vector _bursts; + Ui::Animations::Simple _showAnimation; + std::optional _textColorOverride; + QColor _cardOverlay; + mutable QImage _starOutline; + mutable QImage _starFilled; + mutable QColor _starColor; + rpl::variable _rating = 0; + int _preview = 0; + int _over = -1; + int _pressed = -1; + +}; + +class EndCloseButton final : public Ui::RippleButton { +public: + explicit EndCloseButton(QWidget *parent); + + void switchToClose(QRect hangupCircle, Fn close); + +private: + void paintEvent(QPaintEvent *e) override; + + QImage prepareRippleMask() const override; + + void prepareFrame(); + + const QString _text; + QRect _from; + Ui::Animations::Simple _animation; + QImage _frame; + +}; + +} // namespace Calls