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 0000000000..eeffb8baa0
Binary files /dev/null and b/Telegram/Resources/animations/call_rate.tgs differ
diff --git a/Telegram/Resources/icons/calls/call_ended.png b/Telegram/Resources/icons/calls/call_ended.png
new file mode 100644
index 0000000000..e0f5646fc3
Binary files /dev/null and b/Telegram/Resources/icons/calls/call_ended.png differ
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 0000000000..9caa18a4a4
Binary files /dev/null and b/Telegram/Resources/icons/calls/call_ended@2x.png differ
diff --git a/Telegram/Resources/icons/calls/call_ended@3x.png b/Telegram/Resources/icons/calls/call_ended@3x.png
new file mode 100644
index 0000000000..d8a0e74b15
Binary files /dev/null and b/Telegram/Resources/icons/calls/call_ended@3x.png differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index c2bd934604..de4808aa96 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -6440,6 +6440,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_call_rate_label" = "Please rate the quality of your call";
"lng_call_rate_comment" = "Comment (optional)";
+"lng_call_rate_title" = "Rate this call";
+"lng_call_rate_description" = "Please rate the quality of this call.";
+"lng_call_ended_title" = "Call ended";
"lng_call_start" = "Start Call";
"lng_call_start_video" = "Start Video";
diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc
index f916e5f387..4fc095e288 100644
--- a/Telegram/Resources/qrc/telegram/animations.qrc
+++ b/Telegram/Resources/qrc/telegram/animations.qrc
@@ -29,6 +29,7 @@
../../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