diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 8b340596dc..e5b20fb088 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4642,6 +4642,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_call_status_failed" = "failed to connect"; "lng_call_status_ringing" = "ringing..."; "lng_call_status_busy" = "line busy"; +"lng_call_status_group_invite" = "Telegram Group Call"; "lng_call_status_sure" = "Click on the Camera icon if you want to start a video call."; "lng_call_fingerprint_tooltip" = "If the emoji on {user}'s screen are the same, this call is 100% secure"; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 3fbdecc1cc..1119b6d69e 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -1543,6 +1543,26 @@ confcallJoinUserpics: UserpicsRow { invert: true; } confcallJoinUserpicsPadding: margins(0px, 0px, 0px, 16px); +confcallInviteVideo: IconButton { + width: 36px; + height: 52px; + + icon: icon {{ "info/info_media_video", groupCallMemberInactiveIcon }}; + iconOver: icon {{ "info/info_media_video", groupCallMemberInactiveIcon }}; + iconPosition: point(-1px, -1px); + + ripple: groupCallRipple; + rippleAreaPosition: point(0px, 8px); + rippleAreaSize: 36px; +} +confcallInviteVideoActive: icon {{ "info/info_media_video", groupCallActiveFg }}; +confcallInviteVideoMargins: margins(0px, 0px, 10px, 0px); +confcallInviteAudio: IconButton(confcallInviteVideo) { + icon: icon {{ "menu/phone", groupCallMemberInactiveIcon }}; + iconOver: icon {{ "menu/phone", groupCallMemberInactiveIcon }}; +} +confcallInviteAudioActive: icon {{ "menu/phone", groupCallActiveFg }}; +confcallInviteAudioMargins: margins(0px, 0px, 4px, 0px); groupCallLinkBox: Box(confcallLinkBox) { bg: groupCallMembersBg; diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 4f81f6f37f..32beb069bd 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/platform/base_platform_info.h" #include "base/random.h" #include "boxes/abstract_box.h" +#include "calls/group/calls_group_common.h" #include "calls/calls_instance.h" #include "calls/calls_panel.h" #include "core/application.h" @@ -251,7 +252,8 @@ Call::Call( not_null delegate, not_null user, CallId conferenceId, - MsgId conferenceInviteMsgId) + MsgId conferenceInviteMsgId, + bool video) : _delegate(delegate) , _user(user) , _api(&_user->session().mtp()) @@ -279,10 +281,10 @@ Call::Call( , _conferenceInviteMsgId(conferenceInviteMsgId) , _videoIncoming( std::make_unique( - StartVideoState(false))) + StartVideoState(video))) , _videoOutgoing( std::make_unique( - StartVideoState(false))) { + StartVideoState(video))) { startWaitingTrack(); setupOutgoingVideo(); } diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index 6be26dd259..6483db2fe4 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -106,7 +106,8 @@ public: not_null delegate, not_null user, CallId conferenceId, - MsgId conferenceInviteMsgId); + MsgId conferenceInviteMsgId, + bool video); [[nodiscard]] Type type() const { return _type; diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index f3e9365fd3..c5f8a8c3a9 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -1010,6 +1010,7 @@ void Instance::showConferenceInvite( const auto media = item ? item->media() : nullptr; const auto call = media ? media->call() : nullptr; const auto conferenceId = call ? call->conferenceId : 0; + const auto video = call->video; if (!conferenceId || call->state != Data::CallState::Invitation || user->isSelf()) { @@ -1036,7 +1037,8 @@ void Instance::showConferenceInvite( delegate, user, conferenceId, - conferenceInviteMsgId); + conferenceInviteMsgId, + video); const auto raw = call.get(); user->session().account().sessionChanges( diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index 9936c69c92..1c46af7a77 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -56,6 +56,7 @@ enum class CallType; class GroupCall; class Panel; struct DhConfig; +struct InviteRequest; struct StartGroupCallArgs { enum class JoinConfirm { @@ -73,7 +74,7 @@ struct StartConferenceCallArgs { std::shared_ptr e2e; QString linkSlug; MsgId joinMessageId; - std::vector> invite; + std::vector invite; bool migrating = false; }; diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 82190632a0..23fe1975e9 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -467,7 +467,7 @@ void Panel::initControls() { *creating = false; } }; - const auto create = [=](std::vector> users) { + const auto create = [=](std::vector users) { if (*creating) { return; } @@ -481,7 +481,7 @@ void Panel::initControls() { }); }; const auto invite = crl::guard(call, [=]( - std::vector> users) { + std::vector users) { create(std::move(users)); }); const auto share = crl::guard(call, [=] { @@ -1285,7 +1285,9 @@ void Panel::paint(QRect clip) { bool Panel::handleClose() const { if (_call) { if (_call->state() == Call::State::WaitingUserConfirmation - || _call->state() == Call::State::Busy) { + || _call->state() == Call::State::Busy + || _call->state() == Call::State::Starting + || _call->state() == Call::State::WaitingIncoming) { _call->hangup(); } else { window()->hide(); @@ -1424,7 +1426,10 @@ void Panel::updateStatusText(State state) { case State::ExchangingKeys: return tr::lng_call_status_exchanging(tr::now); case State::Waiting: return tr::lng_call_status_waiting(tr::now); case State::Requesting: return tr::lng_call_status_requesting(tr::now); - case State::WaitingIncoming: return tr::lng_call_status_incoming(tr::now); + case State::WaitingIncoming: + return (_call->conferenceInvite() + ? tr::lng_call_status_group_invite(tr::now) + : tr::lng_call_status_incoming(tr::now)); case State::Ringing: return tr::lng_call_status_ringing(tr::now); case State::Busy: return tr::lng_call_status_busy(tr::now); case State::WaitingUserConfirmation: return tr::lng_call_status_sure(tr::now); diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index 057177db83..832b5e8fad 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -3712,7 +3712,7 @@ void GroupCall::editParticipant( } void GroupCall::inviteUsers( - const std::vector> &users, + const std::vector &requests, Fn done) { const auto real = lookupReal(); if (!real) { @@ -3737,9 +3737,11 @@ void GroupCall::inviteUsers( }; if (const auto call = _conferenceCall.get()) { - for (const auto &user : users) { + for (const auto &request : requests) { + using Flag = MTPphone_InviteConferenceCallParticipant::Flag; + const auto user = request.user; _api.request(MTPphone_InviteConferenceCallParticipant( - MTP_flags(0), + MTP_flags(request.video ? Flag::f_video : Flag()), inputCallSafe(), user->inputUser )).done([=](const MTPUpdates &result) { @@ -3785,7 +3787,8 @@ void GroupCall::inviteUsers( slice.clear(); usersSlice.clear(); }; - for (const auto &user : users) { + for (const auto &request : requests) { + const auto user = request.user; owner->registerInvitedToCallUser(_id, _peer, user); usersSlice.push_back(user); slice.push_back(user->inputUser); @@ -3920,7 +3923,7 @@ void GroupCall::destroyScreencast() { } TextWithEntities ComposeInviteResultToast( - const GroupCall::InviteResult &result) { + const InviteResult &result) { auto text = TextWithEntities(); const auto invited = int(result.invited.size()); const auto restricted = int(result.privacyRestricted.size()); diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index f30abd8b17..2b5be388fb 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -60,6 +60,9 @@ enum class VideoQuality; enum class Error; } // namespace Group +struct InviteRequest; +struct InviteResult; + enum class MuteState { Active, PushToTalk, @@ -418,13 +421,8 @@ public: void toggleMute(const Group::MuteRequest &data); void changeVolume(const Group::VolumeRequest &data); - struct InviteResult { - std::vector> invited; - std::vector> alreadyIn; - std::vector> privacyRestricted; - }; void inviteUsers( - const std::vector> &users, + const std::vector &requests, Fn done); std::shared_ptr ensureGlobalShortcutManager(); @@ -754,6 +752,6 @@ private: }; [[nodiscard]] TextWithEntities ComposeInviteResultToast( - const GroupCall::InviteResult &result); + const InviteResult &result); } // namespace Calls diff --git a/Telegram/SourceFiles/calls/group/calls_group_common.h b/Telegram/SourceFiles/calls/group/calls_group_common.h index 0d40b1f14d..63ab74dd89 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_common.h +++ b/Telegram/SourceFiles/calls/group/calls_group_common.h @@ -41,6 +41,20 @@ namespace Window { class SessionController; } // namespace Window +namespace Calls { + +struct InviteRequest { + not_null user; + bool video = false; +}; +struct InviteResult { + std::vector> invited; + std::vector> alreadyIn; + std::vector> privacyRestricted; +}; + +} // namespace Calls + namespace Calls::Group { constexpr auto kDefaultVolume = 10000; @@ -146,7 +160,7 @@ struct ConferenceCallLinkArgs { bool joining = false; bool migrating = false; Fn finished; - std::vector> invite; + std::vector invite; ConferenceCallLinkStyleOverrides st; }; void ShowConferenceCallLinkBox( @@ -163,7 +177,7 @@ void ExportConferenceCallLink( struct ConferenceFactoryArgs { std::shared_ptr show; Fn finished; - std::vector> invite; + std::vector invite; bool joining = false; bool migrating = false; }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index c8b2601270..05477bd8e8 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_common.h" #include "calls/group/calls_group_menu.h" #include "calls/calls_call.h" #include "boxes/peer_lists_box.h" @@ -19,10 +20,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_icon.h" #include "main/main_session.h" #include "main/session/session_show.h" +#include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/layers/generic_box.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" +#include "ui/painter.h" #include "apiwrap.h" #include "lang/lang_keys.h" #include "styles/style_boxes.h" // membersMarginTop @@ -61,6 +64,36 @@ namespace { return result; } +class ConfInviteRow final : public PeerListRow { +public: + using PeerListRow::PeerListRow; + + void setAlreadyIn(bool alreadyIn); + void setVideo(bool video); + + int elementsCount() const override; + QRect elementGeometry(int element, int outerWidth) const override; + bool elementDisabled(int element) const override; + bool elementOnlySelect(int element) const override; + void elementAddRipple( + int element, + QPoint point, + Fn updateCallback) override; + void elementsStopLastRipple() override; + void elementsPaint( + Painter &p, + int outerWidth, + bool selected, + int selectedElement) override; + +private: + std::unique_ptr _videoRipple; + std::unique_ptr _audioRipple; + bool _alreadyIn = false; + bool _video = false; + +}; + class ConfInviteController final : public ContactsBoxController { public: ConfInviteController( @@ -69,6 +102,8 @@ public: Fn shareLink); [[nodiscard]] rpl::producer hasSelectedValue() const; + [[nodiscard]] std::vector requests( + const std::vector> &peers) const; protected: void prepareViewHook() override; @@ -77,16 +112,132 @@ protected: not_null user) override; void rowClicked(not_null row) override; + void rowElementClicked(not_null row, int element) override; private: [[nodiscard]] int fullCount() const; + void toggleRowSelected(not_null row, bool video); const base::flat_set> _alreadyIn; const Fn _shareLink; rpl::variable _hasSelected; + base::flat_set> _withVideo; + bool _lastSelectWithVideo = false; }; +void ConfInviteRow::setAlreadyIn(bool alreadyIn) { + _alreadyIn = alreadyIn; + setDisabledState(alreadyIn ? State::DisabledChecked : State::Active); +} + +void ConfInviteRow::setVideo(bool video) { + _video = video; +} + +int ConfInviteRow::elementsCount() const { + return _alreadyIn ? 0 : 2; +} + +QRect ConfInviteRow::elementGeometry(int element, int outerWidth) const { + if (_alreadyIn || (element != 1 && element != 2)) { + return QRect(); + } + const auto &st = (element == 1) + ? st::confcallInviteVideo + : st::confcallInviteAudio; + const auto size = QSize(st.width, st.height); + const auto margins = (element == 1) + ? st::confcallInviteVideoMargins + : st::confcallInviteAudioMargins; + const auto right = margins.right(); + const auto top = margins.top(); + const auto side = (element == 1) + ? outerWidth + : elementGeometry(1, outerWidth).x(); + const auto left = side - right - size.width(); + return QRect(QPoint(left, top), size); +} + +bool ConfInviteRow::elementDisabled(int element) const { + return _alreadyIn + || (checked() + && ((_video && element == 1) || (!_video && element == 2))); +} + +bool ConfInviteRow::elementOnlySelect(int element) const { + return false; +} + +void ConfInviteRow::elementAddRipple( + int element, + QPoint point, + Fn updateCallback) { + if (_alreadyIn || (element != 1 && element != 2)) { + return; + } + auto &ripple = (element == 1) ? _videoRipple : _audioRipple; + const auto &st = (element == 1) + ? st::confcallInviteVideo + : st::confcallInviteAudio; + if (!ripple) { + auto mask = Ui::RippleAnimation::EllipseMask(QSize( + st.rippleAreaSize, + st.rippleAreaSize)); + ripple = std::make_unique( + st.ripple, + std::move(mask), + std::move(updateCallback)); + } + ripple->add(point - st.rippleAreaPosition); +} + +void ConfInviteRow::elementsStopLastRipple() { + if (_videoRipple) { + _videoRipple->lastStop(); + } + if (_audioRipple) { + _audioRipple->lastStop(); + } +} + +void ConfInviteRow::elementsPaint( + Painter &p, + int outerWidth, + bool selected, + int selectedElement) { + if (_alreadyIn) { + return; + } + const auto paintElement = [&](int element) { + const auto &st = (element == 1) + ? st::confcallInviteVideo + : st::confcallInviteAudio; + auto &ripple = (element == 1) ? _videoRipple : _audioRipple; + const auto active = checked() && ((element == 1) ? _video : !_video); + const auto geometry = elementGeometry(element, outerWidth); + if (ripple) { + ripple->paint( + p, + geometry.x() + st.rippleAreaPosition.x(), + geometry.y() + st.rippleAreaPosition.y(), + outerWidth); + if (ripple->empty()) { + ripple.reset(); + } + } + const auto selected = (element == selectedElement); + const auto &icon = active + ? (element == 1 + ? st::confcallInviteVideoActive + : st::confcallInviteAudioActive) + : (selected ? st.iconOver : st.icon); + icon.paintInCenter(p, geometry); + }; + paintElement(1); + paintElement(2); +} + ConfInviteController::ConfInviteController( not_null session, base::flat_set> alreadyIn, @@ -100,6 +251,18 @@ rpl::producer ConfInviteController::hasSelectedValue() const { return _hasSelected.value(); } +std::vector ConfInviteController::requests( + const std::vector> &peers) const { + auto result = std::vector(); + result.reserve(peers.size()); + for (const auto &peer : peers) { + if (const auto user = peer->asUser()) { + result.push_back({ user, _withVideo.contains(user) }); + } + } + return result; +} + std::unique_ptr ConfInviteController::createRow( not_null user) { if (user->isSelf() @@ -108,9 +271,12 @@ std::unique_ptr ConfInviteController::createRow( || user->isInaccessible()) { return nullptr; } - auto result = ContactsBoxController::createRow(user); + auto result = std::make_unique(user); if (_alreadyIn.contains(user)) { - result->setDisabledState(PeerListRow::State::DisabledChecked); + result->setAlreadyIn(true); + } + if (_withVideo.contains(user)) { + result->setVideo(true); } return result; } @@ -120,10 +286,42 @@ int ConfInviteController::fullCount() const { } void ConfInviteController::rowClicked(not_null row) { + toggleRowSelected(row, _lastSelectWithVideo); +} + +void ConfInviteController::rowElementClicked( + not_null row, + int element) { + if (row->checked()) { + static_cast(row.get())->setVideo(element == 1); + _lastSelectWithVideo = (element == 1); + } else if (element == 1) { + toggleRowSelected(row, true); + } else if (element == 2) { + toggleRowSelected(row, false); + } +} + +void ConfInviteController::toggleRowSelected( + not_null row, + bool video) { auto count = fullCount(); auto limit = Data::kMaxConferenceMembers; if (count < limit || row->checked()) { + const auto real = static_cast(row.get()); + if (!row->checked()) { + real->setVideo(video); + _lastSelectWithVideo = video; + } + const auto user = row->peer()->asUser(); + if (!row->checked() && video) { + _withVideo.emplace(user); + } else { + _withVideo.remove(user); + } delegate()->peerListSetRowChecked(row, !row->checked()); + + // row may have been destroyed here, from search. _hasSelected = (delegate()->peerListSelectedRowsCount() > 0); } else { delegate()->peerListUiShow()->showToast( @@ -311,13 +509,7 @@ object_ptr PrepareInviteBox( if (!call) { return; } - auto peers = box->collectSelectedRows(); - auto users = ranges::views::all( - peers - ) | ranges::views::transform([](not_null peer) { - return not_null(peer->asUser()); - }) | ranges::to_vector; - const auto done = [=](GroupCall::InviteResult result) { + const auto done = [=](InviteResult result) { (*close)(); if (result.invited.empty() && result.privacyRestricted.empty()) { @@ -325,7 +517,9 @@ object_ptr PrepareInviteBox( } showToast({ ComposeInviteResultToast(result) }); }; - call->inviteUsers(users, done); + call->inviteUsers( + raw->requests(box->collectSelectedRows()), + done); }); } box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); @@ -354,7 +548,12 @@ object_ptr PrepareInviteBox( if (!call) { return; } - call->inviteUsers(users, [=](GroupCall::InviteResult result) { + auto requests = ranges::views::all( + users + ) | ranges::views::transform([](not_null user) { + return InviteRequest{ user }; + }) | ranges::to_vector; + call->inviteUsers(std::move(requests), [=](InviteResult result) { if (result.invited.size() == 1) { showToast(tr::lng_group_call_invite_done_user( tr::now, @@ -463,7 +662,7 @@ object_ptr PrepareInviteBox( object_ptr PrepareInviteBox( not_null call, - Fn>)> inviteUsers, + Fn)> inviteUsers, Fn shareLink) { const auto user = call->user(); const auto weak = base::make_weak(call); @@ -486,13 +685,7 @@ object_ptr PrepareInviteBox( if (!call) { return; } - auto peers = box->collectSelectedRows(); - auto users = ranges::views::all( - peers - ) | ranges::views::transform([](not_null peer) { - return not_null(peer->asUser()); - }) | ranges::to_vector; - inviteUsers(std::move(users)); + inviteUsers(raw->requests(box->collectSelectedRows())); }); } box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h index b41fbfba67..8c136d19f7 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Calls { class Call; class GroupCall; +struct InviteRequest; } // namespace Calls namespace Calls::Group { @@ -83,7 +84,7 @@ private: [[nodiscard]] object_ptr PrepareInviteBox( not_null call, - Fn>)> inviteUsers, + Fn)> inviteUsers, Fn shareLink); } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 7e3fe18dbb..418d13943c 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -1006,8 +1006,8 @@ void Panel::migrationShowShareLink() { { .st = DarkConferenceCallLinkStyle() }); } -void Panel::migrationInviteUsers(std::vector> users) { - const auto done = [=](GroupCall::InviteResult result) { +void Panel::migrationInviteUsers(std::vector users) { + const auto done = [=](InviteResult result) { showToast({ ComposeInviteResultToast(result) }); }; _call->inviteUsers(std::move(users), crl::guard(this, done)); diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index 2af708af80..86108ca4b8 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -73,6 +73,10 @@ struct CallSignalBars; struct CallBodyLayout; } // namespace style +namespace Calls { +struct InviteRequest; +} // namespace Calls + namespace Calls::Group { class Toasts; @@ -116,7 +120,7 @@ public: [[nodiscard]] bool isLayerShown() const; void migrationShowShareLink(); - void migrationInviteUsers(std::vector> users); + void migrationInviteUsers(std::vector users); void minimize(); void toggleFullScreen(); diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 8ef7f28c59..969cbd2340 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -491,6 +491,7 @@ Call ComputeCallData(const MTPDmessageActionConferenceCall &call) { : call.is_active() ? CallState::Active : CallState::Invitation), + .video = call.is_video(), }; }