diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4a3f1bc052..86ea166e86 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4655,6 +4655,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_call_error_camera_not_started" = "You can switch to video call once you're connected."; "lng_call_error_camera_outdated" = "{user}'s app does not support video calls. They need to update their app before you can call them."; "lng_call_error_audio_io" = "There seems to be a problem with your sound card. Please make sure that your computer's speakers and microphone are working and try again."; +"lng_call_error_add_not_started" = "You can add more people once you're connected."; "lng_call_bar_hangup" = "End call"; "lng_call_leave_to_other_sure" = "End your active call and join this video chat?"; @@ -4691,6 +4692,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_call_start_video" = "Start Video"; "lng_call_stop_video" = "Stop Video"; "lng_call_screencast" = "Screencast"; +"lng_call_add_people" = "Add People"; "lng_call_end_call" = "End Call"; "lng_call_mute_audio" = "Mute"; "lng_call_unmute_audio" = "Unmute"; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index d67b208f9b..9c8e600509 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -24,7 +24,7 @@ CallSignalBars { inactiveOpacity: double; } -callWidthMin: 300px; +callWidthMin: 372px; callHeightMin: 440px; callWidth: 720px; callHeight: 540px; @@ -183,6 +183,18 @@ callCameraUnmute: CallButton(callMicrophoneUnmute) { } } } +callAddPeople: CallButton(callAnswer) { + button: IconButton(callButton) { + icon: icon {{ "settings/group", callIconFgActive }}; + iconPosition: point(-1px, 24px); + ripple: RippleAnimation(defaultRippleAnimation) { + color: callIconActiveRipple; + } + } + bg: callIconBgActive; + outerBg: callIconBgActive; + label: callButtonLabel; +} callCornerButtonInner: IconButton { width: 20px; height: 20px; diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 3a34f07836..2ff549c93d 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_panel.h" #include "core/application.h" #include "core/core_settings.h" +#include "data/data_group_call.h" #include "data/data_session.h" #include "data/data_user.h" #include "lang/lang_keys.h" @@ -527,20 +528,23 @@ crl::time Call::getDurationMs() const { return _startTime ? (crl::now() - _startTime) : 0; } -void Call::hangup() { +void Call::hangup(Data::GroupCall *migrateCall, const QString &migrateSlug) { const auto state = _state.current(); - if (state == State::Busy) { + if (state == State::Busy || state == State::MigrationHangingUp) { _delegate->callFinished(this); } else { const auto missed = (state == State::Ringing || (state == State::Waiting && _type == Type::Outgoing)); const auto declined = isIncomingWaiting(); - const auto reason = missed + const auto reason = !migrateSlug.isEmpty() + ? MTP_phoneCallDiscardReasonMigrateConferenceCall( + MTP_string(migrateSlug)) + : missed ? MTP_phoneCallDiscardReasonMissed() : declined ? MTP_phoneCallDiscardReasonBusy() : MTP_phoneCallDiscardReasonHangup(); - finish(FinishType::Ended, reason); + finish(FinishType::Ended, reason, migrateCall); } } @@ -740,7 +744,10 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { && reason->type() == mtpc_phoneCallDiscardReasonDisconnect) { LOG(("Call Info: Discarded with DISCONNECT reason.")); } - if (reason && reason->type() == mtpc_phoneCallDiscardReasonBusy) { + if (reason && reason->type() == mtpc_phoneCallDiscardReasonMigrateConferenceCall) { + const auto slug = qs(reason->c_phoneCallDiscardReasonMigrateConferenceCall().vslug()); + finishByMigration(slug); + } else if (reason && reason->type() == mtpc_phoneCallDiscardReasonBusy) { setState(State::Busy); } else if (_type == Type::Outgoing || _state.current() == State::HangingUp) { @@ -768,6 +775,32 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { Unexpected("phoneCall type inside an existing call handleUpdate()"); } +void Call::finishByMigration(const QString &slug) { + if (_state.current() == State::MigrationHangingUp) { + return; + } + setState(State::MigrationHangingUp); + const auto limit = 5; + const auto session = &_user->session(); + session->api().request(MTPphone_GetGroupCall( + MTP_inputGroupCallSlug(MTP_string(slug)), + MTP_int(limit) + )).done([=](const MTPphone_GroupCall &result) { + result.data().vcall().match([&](const auto &data) { + const auto call = session->data().sharedConferenceCall( + data.vid().v, + data.vaccess_hash().v); + call->processFullCall(result); + Core::App().calls().startOrJoinConferenceCall({ + .call = call, + .linkSlug = slug, + }); + }); + }).fail(crl::guard(this, [=] { + setState(State::Failed); + })).send(); +} + void Call::updateRemoteMediaState( tgcalls::AudioState audio, tgcalls::VideoState video) { @@ -1059,6 +1092,7 @@ void Call::createAndStartController(const MTPDphoneCall &call) { const auto track = (state != State::FailedHangingUp) && (state != State::Failed) && (state != State::HangingUp) + && (state != State::MigrationHangingUp) && (state != State::Ended) && (state != State::EndedByOtherDevice) && (state != State::Busy); @@ -1175,6 +1209,11 @@ void Call::setState(State state) { && state != State::Failed) { return; } + if (was == State::MigrationHangingUp + && state != State::Ended + && state != State::Failed) { + return; + } if (was != state) { _state = state; @@ -1323,7 +1362,10 @@ rpl::producer Call::cameraDeviceIdValue() const { return _cameraDeviceId.value(); } -void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) { +void Call::finish( + FinishType type, + const MTPPhoneCallDiscardReason &reason, + Data::GroupCall *migrateCall) { Expects(type != FinishType::None); setSignalBarCount(kSignalBarFinished); @@ -1371,6 +1413,12 @@ void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) { // We want to discard request still being sent and processed even if // the call is already destroyed. + if (migrateCall) { + _user->owner().registerInvitedToCallUser( + migrateCall->id(), + migrateCall, + _user); + } const auto session = &_user->session(); const auto weak = base::make_weak(this); session->api().request(MTPphone_DiscardCall( // We send 'discard' here. diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index 1d01d6d5f5..2e024cea83 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtproto_auth_key.h" #include "webrtc/webrtc_device_resolver.h" +namespace Data { +class GroupCall; +} // namespace Data + namespace Media { namespace Audio { class Track; @@ -122,6 +126,7 @@ public: FailedHangingUp, Failed, HangingUp, + MigrationHangingUp, Ended, EndedByOtherDevice, ExchangingKeys, @@ -198,7 +203,9 @@ public: void applyUserConfirmation(); void answer(); - void hangup(); + void hangup( + Data::GroupCall *migrateCall = nullptr, + const QString &migrateSlug = QString()); void redial(); bool isKeyShaForFingerprintReady() const; @@ -246,7 +253,9 @@ private: void finish( FinishType type, const MTPPhoneCallDiscardReason &reason - = MTP_phoneCallDiscardReasonDisconnect()); + = MTP_phoneCallDiscardReasonDisconnect(), + Data::GroupCall *migrateCall = nullptr); + void finishByMigration(const QString &slug); void startOutgoing(); void startIncoming(); void startWaitingTrack(); diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 9a3147e76c..effbcfb8bb 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -230,12 +230,13 @@ void Instance::startOrJoinGroupCall( }); } -void Instance::startOrJoinConferenceCall( - std::shared_ptr show, - StartConferenceCallArgs args) { - destroyCurrentCall(); +void Instance::startOrJoinConferenceCall(StartConferenceCallArgs args) { + destroyCurrentCall( + args.migrating ? args.call.get() : nullptr, + args.migrating ? args.linkSlug : QString()); const auto session = &args.call->peer()->session(); + const auto showShareLink = args.migrating && args.invite.empty(); auto call = std::make_unique( _delegate.get(), Calls::Group::ConferenceInfo{ @@ -254,6 +255,11 @@ void Instance::startOrJoinConferenceCall( _currentGroupCallPanel = std::make_unique(raw); _currentGroupCall = std::move(call); _currentGroupCallChanges.fire_copy(raw); + if (!args.invite.empty()) { + _currentGroupCallPanel->migrationInviteUsers(std::move(args.invite)); + } else if (args.migrating) { + _currentGroupCallPanel->migrationShowShareLink(); + } } void Instance::confirmLeaveCurrent( @@ -435,24 +441,6 @@ void Instance::createGroupCall( _currentGroupCallChanges.fire_copy(raw); } -void Instance::createConferenceCall(Group::ConferenceInfo info) { - destroyCurrentCall(); - - auto call = std::make_unique( - _delegate.get(), - std::move(info)); - const auto raw = call.get(); - - raw->peer()->session().account().sessionChanges( - ) | rpl::start_with_next([=] { - destroyGroupCall(raw); - }, raw->lifetime()); - - _currentGroupCallPanel = std::make_unique(raw); - _currentGroupCall = std::move(call); - _currentGroupCallChanges.fire_copy(raw); -} - void Instance::refreshDhConfig() { Expects(_currentCall != nullptr); @@ -744,9 +732,11 @@ bool Instance::inGroupCall() const { && (state != GroupCall::State::Failed); } -void Instance::destroyCurrentCall() { +void Instance::destroyCurrentCall( + Data::GroupCall *migrateCall, + const QString &migrateSlug) { if (const auto current = currentCall()) { - current->hangup(); + current->hangup(migrateCall, migrateSlug); if (const auto still = currentCall()) { destroyCall(still); } diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index 2d5df76f2a..ad55bb5dec 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -73,6 +73,8 @@ struct StartConferenceCallArgs { std::shared_ptr e2e; QString linkSlug; MsgId joinMessageId; + std::vector> invite; + bool migrating = false; }; class Instance final : public base::has_weak_ptr { @@ -85,9 +87,7 @@ public: std::shared_ptr show, not_null peer, StartGroupCallArgs args); - void startOrJoinConferenceCall( - std::shared_ptr show, - StartConferenceCallArgs args); + void startOrJoinConferenceCall(StartConferenceCallArgs args); void showStartWithRtmp( std::shared_ptr show, not_null peer); @@ -140,7 +140,6 @@ private: void createGroupCall( Group::JoinInfo info, const MTPInputGroupCall &inputCall); - void createConferenceCall(Group::ConferenceInfo info); void destroyGroupCall(not_null call); void confirmLeaveCurrent( std::shared_ptr show, @@ -156,7 +155,9 @@ private: void refreshServerConfig(not_null session); bytes::const_span updateDhConfig(const MTPmessages_DhConfig &data); - void destroyCurrentCall(); + void destroyCurrentCall( + Data::GroupCall *migrateCall = nullptr, + const QString &migrateSlug = QString()); void handleCallUpdate( not_null session, const MTPPhoneCall &call); diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index e5af06c171..1b48630320 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_cloud_file.h" #include "data/data_changes.h" #include "calls/group/calls_group_common.h" +#include "calls/group/calls_group_invite_controller.h" #include "calls/ui/calls_device_menu.h" #include "calls/calls_emoji_fingerprint.h" #include "calls/calls_signal_bars.h" @@ -45,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/integration.h" #include "core/application.h" #include "lang/lang_keys.h" +#include "main/session/session_show.h" #include "main/main_session.h" #include "apiwrap.h" #include "platform/platform_specific.h" @@ -92,6 +94,77 @@ constexpr auto kHideControlsQuickTimeout = 2 * crl::time(1000); )"; } +class Show final : public Main::SessionShow { +public: + explicit Show(not_null panel); + ~Show(); + + void showOrHideBoxOrLayer( + std::variant< + v::null_t, + object_ptr, + std::unique_ptr> &&layer, + Ui::LayerOptions options, + anim::type animated) const override; + [[nodiscard]] not_null toastParent() const override; + [[nodiscard]] bool valid() const override; + operator bool() const override; + + [[nodiscard]] Main::Session &session() const override; + +private: + const base::weak_ptr _panel; + +}; + +Show::Show(not_null panel) +: _panel(base::make_weak(panel)) { +} + +Show::~Show() = default; + +void Show::showOrHideBoxOrLayer( + std::variant< + v::null_t, + object_ptr, + std::unique_ptr> &&layer, + Ui::LayerOptions options, + anim::type animated) const { + using UniqueLayer = std::unique_ptr; + using ObjectBox = object_ptr; + if (auto layerWidget = std::get_if(&layer)) { + if (const auto panel = _panel.get()) { + panel->showLayer(std::move(*layerWidget), options, animated); + } + } else if (auto box = std::get_if(&layer)) { + if (const auto panel = _panel.get()) { + panel->showBox(std::move(*box), options, animated); + } + } else if (const auto panel = _panel.get()) { + panel->hideLayer(animated); + } +} + +not_null Show::toastParent() const { + const auto panel = _panel.get(); + Assert(panel != nullptr); + return panel->widget(); +} + +bool Show::valid() const { + return !_panel.empty(); +} + +Show::operator bool() const { + return valid(); +} + +Main::Session &Show::session() const { + const auto panel = _panel.get(); + Assert(panel != nullptr); + return panel->user()->session(); +} + } // namespace Panel::Panel(not_null call) @@ -121,6 +194,9 @@ Panel::Panel(not_null call) widget(), st::callMicrophoneMute, &st::callMicrophoneUnmute)) +, _addPeople( + widget(), + object_ptr(widget(), st::callAddPeople)) , _name(widget(), st::callName) , _status(widget(), st::callStatus) , _hideControlsTimer([=] { requestControlsHidden(true); }) @@ -133,6 +209,8 @@ Panel::Panel(not_null call) _cancel->setDuration(st::callPanelDuration); _cancel->entity()->setText(tr::lng_call_cancel()); _screencast->setDuration(st::callPanelDuration); + _addPeople->setDuration(st::callPanelDuration); + _addPeople->entity()->setText(tr::lng_call_add_people()); initWindow(); initWidget(); @@ -153,6 +231,62 @@ bool Panel::isActive() const { return window()->isActiveWindow() && isVisible(); } +base::weak_ptr Panel::showToast( + const QString &text, + crl::time duration) { + return showToast({ + .text = { text }, + .duration = duration, + }); +} + +base::weak_ptr Panel::showToast( + TextWithEntities &&text, + crl::time duration) { + return showToast({ + .text = std::move(text), + .duration = duration, + }); +} + +base::weak_ptr Panel::showToast( + Ui::Toast::Config &&config) { + if (!config.st) { + config.st = &st::callErrorToast; + } + return Show(this).showToast(std::move(config)); +} + +void Panel::showBox(object_ptr box) { + showBox(std::move(box), Ui::LayerOption::KeepOther, anim::type::normal); +} + +void Panel::showBox( + object_ptr box, + Ui::LayerOptions options, + anim::type animated) { + _layerBg->showBox(std::move(box), options, animated); +} + +void Panel::showLayer( + std::unique_ptr layer, + Ui::LayerOptions options, + anim::type animated) { + _layerBg->showLayer(std::move(layer), options, animated); +} + +void Panel::hideLayer(anim::type animated) { + _layerBg->hideAll(animated); +} + +bool Panel::isLayerShown() const { + return _layerBg->topShownLayer() != nullptr; +} + +std::shared_ptr Panel::uiShow() { + return std::make_shared(this); +} + void Panel::showAndActivate() { if (window()->isHidden()) { window()->show(); @@ -303,7 +437,7 @@ void Panel::initControls() { return; } else if (!env->desktopCaptureAllowed()) { if (auto box = Group::ScreenSharingPrivacyRequestBox()) { - _layerBg->showBox(std::move(box)); + showBox(std::move(box)); } } else if (const auto source = env->uniqueDesktopCaptureSource()) { if (!chooseSourceActiveDeviceId().isEmpty()) { @@ -318,9 +452,42 @@ void Panel::initControls() { _camera->setClickedCallback([=] { if (!_call) { return; - } else { - _call->toggleCameraSharing(!_call->isSharingCamera()); } + _call->toggleCameraSharing(!_call->isSharingCamera()); + }); + _addPeople->entity()->setClickedCallback([=] { + if (!_call || _call->state() != Call::State::Established) { + showToast(tr::lng_call_error_add_not_started(tr::now)); + return; + } + const auto call = _call; + const auto creating = std::make_shared(); + const auto finish = [=](QString link) { + if (link.isEmpty()) { + *creating = false; + } + }; + const auto create = [=](std::vector> users) { + if (*creating) { + return; + } + *creating = true; + Group::MakeConferenceCall({ + .show = uiShow(), + .finished = finish, + .invite = std::move(users), + .joining = true, + .migrating = true, + }); + }; + const auto invite = crl::guard(call, [=]( + std::vector> users) { + create(std::move(users)); + }); + const auto share = crl::guard(call, [=] { + create({}); + }); + showBox(Group::PrepareInviteBox(call, invite, share)); }); _updateDurationTimer.setCallback([this] { @@ -605,6 +772,7 @@ void Panel::reinitWithCall(Call *call) { && state != State::EndedByOtherDevice && state != State::Failed && state != State::FailedHangingUp + && state != State::MigrationHangingUp && state != State::HangingUp) { refreshOutgoingPreviewInBody(state); } @@ -630,10 +798,7 @@ void Panel::reinitWithCall(Call *call) { } Unexpected("Error type in _call->errors()."); }(); - Ui::Toast::Show(widget(), Ui::Toast::Config{ - .text = { text }, - .st = &st::callErrorToast, - }); + showToast(text); }, _callLifetime); _name->setText(_user->name()); @@ -647,6 +812,7 @@ void Panel::reinitWithCall(Call *call) { _startVideo->raise(); } _mute->raise(); + _addPeople->raise(); _powerSaveBlocker = std::make_unique( base::PowerSaveBlockType::PreventDisplaySleep, @@ -1077,11 +1243,7 @@ void Panel::updateHangupGeometry() { // Screencast - Camera - Cancel/Decline - Answer/Hangup/Redial - Mute. const auto buttonWidth = st::callCancel.button.width; const auto cancelWidth = buttonWidth * (1. - hangupProgress); - const auto cancelLeft = (isWaitingUser) - ? ((widget()->width() - buttonWidth) / 2) - : (_mute->animating()) - ? ((widget()->width() - cancelWidth) / 2) - : ((widget()->width() / 2) - cancelWidth); + const auto cancelLeft = (widget()->width() - buttonWidth) / 2; _cancel->moveToLeft(cancelLeft, _buttonsTop); _decline->moveToLeft(cancelLeft, _buttonsTop); @@ -1089,6 +1251,7 @@ void Panel::updateHangupGeometry() { _screencast->moveToLeft(_camera->x() - buttonWidth, _buttonsTop); _answerHangupRedial->moveToLeft(cancelLeft + cancelWidth, _buttonsTop); _mute->moveToLeft(_answerHangupRedial->x() + buttonWidth, _buttonsTop); + _addPeople->moveToLeft(_mute->x() + buttonWidth, _buttonsTop); if (_startVideo) { _startVideo->moveToLeft(_camera->x(), _camera->y()); } @@ -1136,12 +1299,17 @@ not_null Panel::widget() const { return _window.widget(); } +not_null Panel::user() const { + return _user; +} + void Panel::stateChanged(State state) { Expects(_call != nullptr); updateStatusText(state); if ((state != State::HangingUp) + && (state != State::MigrationHangingUp) && (state != State::Ended) && (state != State::EndedByOtherDevice) && (state != State::FailedHangingUp) @@ -1182,6 +1350,7 @@ void Panel::stateChanged(State state) { toggleButton( _screencast, !(isBusy || isWaitingUser || incomingWaiting)); + toggleButton(_addPeople, !isWaitingUser); const auto hangupShown = !_decline->toggled() && !_cancel->toggled(); if (_hangupShown != hangupShown) { @@ -1232,7 +1401,8 @@ void Panel::updateStatusText(State state) { switch (state) { case State::Starting: case State::WaitingInit: - case State::WaitingInitAck: return tr::lng_call_status_connecting(tr::now); + case State::WaitingInitAck: + case State::MigrationHangingUp: return tr::lng_call_status_connecting(tr::now); case State::Established: { if (_call) { auto durationMs = _call->getDurationMs(); diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index ee24ab2c4a..fdb61333ab 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -27,7 +27,15 @@ namespace Data { class PhotoMedia; } // namespace Data +namespace Main { +class SessionShow; +} // namespace Main + namespace Ui { +class BoxContent; +class LayerWidget; +enum class LayerOption; +using LayerOptions = base::flags; class IconButton; class CallButton; class LayerManager; @@ -38,14 +46,17 @@ template class PaddingWrap; class RpWindow; class PopupMenu; -namespace GL { -enum class Backend; -} // namespace GL -namespace Platform { -struct SeparateTitleControls; -} // namespace Platform } // namespace Ui +namespace Ui::Toast { +class Instance; +struct Config; +} // namespace Ui::Toast + +namespace Ui::Platform { +struct SeparateTitleControls; +} // namespace Ui::Platform + namespace style { struct CallSignalBars; struct CallBodyLayout; @@ -58,13 +69,39 @@ class SignalBars; class VideoBubble; struct DeviceSelection; -class Panel final : private Group::Ui::DesktopCapture::ChooseSourceDelegate { +class Panel final + : public base::has_weak_ptr + , private Group::Ui::DesktopCapture::ChooseSourceDelegate { public: Panel(not_null call); ~Panel(); + [[nodiscard]] not_null widget() const; + [[nodiscard]] not_null user() const; [[nodiscard]] bool isVisible() const; [[nodiscard]] bool isActive() const; + + base::weak_ptr showToast( + const QString &text, + crl::time duration = 0); + base::weak_ptr showToast( + TextWithEntities &&text, + crl::time duration = 0); + base::weak_ptr showToast( + Ui::Toast::Config &&config); + + void showBox(object_ptr box); + void showBox( + object_ptr box, + Ui::LayerOptions options, + anim::type animated = anim::type::normal); + void showLayer( + std::unique_ptr layer, + Ui::LayerOptions options, + anim::type animated = anim::type::normal); + void hideLayer(anim::type animated = anim::type::normal); + [[nodiscard]] bool isLayerShown() const; + void showAndActivate(); void minimize(); void toggleFullScreen(); @@ -83,6 +120,8 @@ public: [[nodiscard]] rpl::producer startOutgoingRequests() const; + [[nodiscard]] std::shared_ptr uiShow(); + [[nodiscard]] rpl::lifetime &lifetime(); private: @@ -97,7 +136,6 @@ private: }; [[nodiscard]] not_null window() const; - [[nodiscard]] not_null widget() const; void paint(QRect clip); @@ -170,6 +208,7 @@ private: base::unique_qptr _startVideo; object_ptr> _mute; Ui::CallButton *_audioDeviceToggle = nullptr; + object_ptr < Ui::FadeWrap> _addPeople; object_ptr _name; object_ptr _status; object_ptr _fingerprint = { nullptr }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index 19950598f4..bd1bf41863 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "lang/lang_hardcoded.h" #include "boxes/peers/edit_participants_box.h" // SubscribeToMigration. +#include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/ui_utility.h" #include "base/unixtime.h" @@ -1164,6 +1165,15 @@ std::shared_ptr GroupCall::conferenceCall() const { return _conferenceCall; } +QString GroupCall::existingConferenceLink() const { + Expects(!_conferenceLinkSlug.isEmpty()); + + const auto session = &_peer->session(); + return !_conferenceLinkSlug.isEmpty() + ? session->createInternalLinkFull("call/" + _conferenceLinkSlug) + : QString(); +} + rpl::producer> GroupCall::real() const { if (const auto real = lookupReal()) { return rpl::single(not_null{ real }); @@ -3902,4 +3912,41 @@ void GroupCall::destroyScreencast() { } } +TextWithEntities ComposeInviteResultToast( + const GroupCall::InviteResult &result) { + auto text = TextWithEntities(); + const auto invited = int(result.invited.size()); + const auto restricted = int(result.privacyRestricted.size()); + if (invited == 1) { + text.append(tr::lng_confcall_invite_done_user( + tr::now, + lt_user, + Ui::Text::Bold(result.invited.front()->shortName()), + Ui::Text::RichLangValue)); + } else if (invited > 1) { + text.append(tr::lng_confcall_invite_done_many( + tr::now, + lt_count, + invited, + Ui::Text::RichLangValue)); + } + if (invited && restricted) { + text.append(u"\n\n"_q); + } + if (restricted == 1) { + text.append(tr::lng_confcall_invite_fail_user( + tr::now, + lt_user, + Ui::Text::Bold(result.privacyRestricted.front()->shortName()), + Ui::Text::RichLangValue)); + } else if (restricted > 1) { + text.append(tr::lng_confcall_invite_fail_many( + tr::now, + lt_count, + restricted, + Ui::Text::RichLangValue)); + } + return text; +} + } // namespace Calls diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index c81ada53c1..f30abd8b17 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -257,6 +257,7 @@ public: [[nodiscard]] Data::GroupCall *lookupReal() const; [[nodiscard]] std::shared_ptr conferenceCall() const; + [[nodiscard]] QString existingConferenceLink() const; [[nodiscard]] rpl::producer> real() const; [[nodiscard]] rpl::producer emojiHashValue() const; @@ -752,4 +753,7 @@ private: }; +[[nodiscard]] TextWithEntities ComposeInviteResultToast( + const GroupCall::InviteResult &result); + } // namespace Calls diff --git a/Telegram/SourceFiles/calls/group/calls_group_common.cpp b/Telegram/SourceFiles/calls/group/calls_group_common.cpp index 110e08d76c..f0dbb9628f 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_common.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_common.cpp @@ -9,9 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/platform/base_platform_info.h" +#include "base/random.h" #include "boxes/share_box.h" +#include "calls/calls_instance.h" +#include "core/application.h" #include "core/local_url_handlers.h" #include "data/data_group_call.h" +#include "data/data_session.h" #include "info/bot/starref/info_bot_starref_common.h" #include "ui/boxes/boost_box.h" #include "ui/widgets/buttons.h" @@ -31,6 +35,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include namespace Calls::Group { +namespace { + +[[nodiscard]] QString ExtractConferenceSlug(const QString &link) { + const auto local = Core::TryConvertUrlToLocal(link); + const auto parts1 = QStringView(local).split('#'); + if (!parts1.isEmpty()) { + const auto parts2 = parts1.front().split('&'); + if (!parts2.isEmpty()) { + const auto parts3 = parts2.front().split(u"slug="_q); + if (parts3.size() > 1) { + return parts3.back().toString(); + } + } + } + return QString(); +} + +} // namespace object_ptr ScreenSharingPrivacyRequestBox() { #ifdef Q_OS_MAC @@ -107,7 +129,6 @@ void ShowConferenceCallLinkBox( ConferenceCallLinkArgs &&args) { const auto st = args.st; const auto initial = args.initial; - const auto weakWindow = args.weakWindow; show->showBox(Box([=](not_null box) { box->setStyle(st.box ? *st.box @@ -151,7 +172,7 @@ void ShowConferenceCallLinkBox( const auto copyCallback = [=] { QApplication::clipboard()->setText(link); - box->uiShow()->showToast(tr::lng_username_copied(tr::now)); + show->showToast(tr::lng_username_copied(tr::now)); }; const auto shareCallback = [=] { FastShareLink( @@ -218,12 +239,11 @@ void ShowConferenceCallLinkBox( : st::confcallLinkCenteredText)); footer->setTryMakeSimilarLines(true); footer->setClickHandlerFilter([=](const auto &...) { - const auto local = Core::TryConvertUrlToLocal(link); - if (const auto controller = weakWindow.get()) { - controller->resolveConferenceCall( - local, - crl::guard(box, [=](bool ok) { if (ok) box->closeBox(); }), - true); + if (auto slug = ExtractConferenceSlug(link); !slug.isEmpty()) { + Core::App().calls().startOrJoinConferenceCall({ + .call = call, + .linkSlug = std::move(slug), + }); } return false; }); @@ -251,6 +271,7 @@ void ExportConferenceCallLink( std::shared_ptr call, ConferenceCallLinkArgs &&args) { const auto session = &show->session(); + const auto invite = std::move(args.invite); const auto finished = std::move(args.finished); using Flag = MTPphone_ExportGroupCallInvite::Flag; @@ -259,18 +280,63 @@ void ExportConferenceCallLink( call->input() )).done([=](const MTPphone_ExportedGroupCallInvite &result) { const auto link = qs(result.data().vlink()); + if (args.joining) { + if (auto slug = ExtractConferenceSlug(link); !slug.isEmpty()) { + Core::App().calls().startOrJoinConferenceCall({ + .call = call, + .linkSlug = std::move(slug), + .invite = invite, + .migrating = args.migrating, + }); + } + if (const auto onstack = finished) { + finished(QString()); + } + return; + } Calls::Group::ShowConferenceCallLinkBox( show, call, link, base::duplicate(args)); if (const auto onstack = finished) { - finished(true); + finished(link); } }).fail([=](const MTP::Error &error) { show->showToast(error.type()); if (const auto onstack = finished) { - finished(false); + finished(QString()); + } + }).send(); +} + +void MakeConferenceCall(ConferenceFactoryArgs &&args) { + const auto show = std::move(args.show); + const auto finished = std::move(args.finished); + const auto joining = args.joining; + const auto migrating = args.migrating; + const auto invite = std::move(args.invite); + const auto session = &show->session(); + session->api().request(MTPphone_CreateConferenceCall( + MTP_int(base::RandomValue()) + )).done([=](const MTPphone_GroupCall &result) { + result.data().vcall().match([&](const auto &data) { + const auto call = session->data().sharedConferenceCall( + data.vid().v, + data.vaccess_hash().v); + call->processFullCall(result); + Calls::Group::ExportConferenceCallLink(show, call, { + .initial = true, + .joining = joining, + .migrating = migrating, + .finished = finished, + .invite = invite, + }); + }); + }).fail([=](const MTP::Error &error) { + show->showToast(error.type()); + if (const auto onstack = finished) { + onstack(QString()); } }).send(); } diff --git a/Telegram/SourceFiles/calls/group/calls_group_common.h b/Telegram/SourceFiles/calls/group/calls_group_common.h index 1591fe5a2a..03c0770d47 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_common.h +++ b/Telegram/SourceFiles/calls/group/calls_group_common.h @@ -142,8 +142,10 @@ struct ConferenceCallLinkStyleOverrides { struct ConferenceCallLinkArgs { bool initial = false; - Fn finished; - base::weak_ptr weakWindow = nullptr; + bool joining = false; + bool migrating = false; + Fn finished; + std::vector> invite; ConferenceCallLinkStyleOverrides st; }; void ShowConferenceCallLinkBox( @@ -157,4 +159,13 @@ void ExportConferenceCallLink( std::shared_ptr call, ConferenceCallLinkArgs &&args); +struct ConferenceFactoryArgs { + std::shared_ptr show; + Fn finished; + std::vector> invite; + bool joining = false; + bool migrating = false; +}; +void MakeConferenceCall(ConferenceFactoryArgs &&args); + } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index 05ef1fc92f..6182597c1c 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -10,6 +10,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_menu.h" +#include "calls/calls_call.h" #include "boxes/peer_lists_box.h" #include "data/data_user.h" #include "data/data_channel.h" @@ -80,7 +81,7 @@ protected: private: [[nodiscard]] int fullCount() const; - base::flat_set> _alreadyIn; + const base::flat_set> _alreadyIn; const Fn _shareLink; rpl::variable _hasSelected; @@ -93,7 +94,6 @@ ConfInviteController::ConfInviteController( : ContactsBoxController(session) , _alreadyIn(std::move(alreadyIn)) , _shareLink(std::move(shareLink)) { - _alreadyIn.remove(session->user()); } rpl::producer ConfInviteController::hasSelectedValue() const { @@ -161,43 +161,6 @@ void ConfInviteController::prepareViewHook() { delegate()->peerListSetAboveWidget(std::move(button)); } -[[nodiscard]] TextWithEntities ComposeInviteResultToast( - const GroupCall::InviteResult &result) { - auto text = TextWithEntities(); - const auto invited = int(result.invited.size()); - const auto restricted = int(result.privacyRestricted.size()); - if (invited == 1) { - text.append(tr::lng_confcall_invite_done_user( - tr::now, - lt_user, - Ui::Text::Bold(result.invited.front()->shortName()), - Ui::Text::RichLangValue)); - } else if (invited > 1) { - text.append(tr::lng_confcall_invite_done_many( - tr::now, - lt_count, - invited, - Ui::Text::RichLangValue)); - } - if (invited && restricted) { - text.append(u"\n\n"_q); - } - if (restricted == 1) { - text.append(tr::lng_confcall_invite_fail_user( - tr::now, - lt_user, - Ui::Text::Bold(result.privacyRestricted.front()->shortName()), - Ui::Text::RichLangValue)); - } else if (restricted > 1) { - text.append(tr::lng_confcall_invite_fail_many( - tr::now, - lt_count, - restricted, - Ui::Text::RichLangValue)); - } - return text; -} - } // namespace InviteController::InviteController( @@ -494,4 +457,44 @@ object_ptr PrepareInviteBox( return Box(std::move(controllers), initBox); } +object_ptr PrepareInviteBox( + not_null call, + Fn>)> inviteUsers, + Fn shareLink) { + const auto user = call->user(); + const auto weak = base::make_weak(call); + auto alreadyIn = base::flat_set>{ user }; + auto controller = std::make_unique( + &user->session(), + alreadyIn, + shareLink); + const auto raw = controller.get(); + raw->setStyleOverrides( + &st::groupCallInviteMembersList, + &st::groupCallMultiSelect); + auto initBox = [=](not_null box) { + box->setTitle(tr::lng_group_call_invite_conf()); + raw->hasSelectedValue() | rpl::start_with_next([=](bool has) { + box->clearButtons(); + if (has) { + box->addButton(tr::lng_group_call_invite_button(), [=] { + const auto call = weak.get(); + 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)); + }); + } + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }, box->lifetime()); + }; + return Box(std::move(controller), initBox); +} + } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h index 47508d131a..b41fbfba67 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/add_participants_box.h" namespace Calls { +class Call; class GroupCall; } // namespace Calls @@ -80,4 +81,9 @@ private: Fn showToast, Fn finished)> shareConferenceLink = nullptr); +[[nodiscard]] object_ptr PrepareInviteBox( + not_null call, + 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 31f55e304a..7e3fe18dbb 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -985,10 +985,10 @@ Fn finished)> Panel::shareConferenceLinkCallback() { return; } *exporting = true; - const auto done = [=](bool ok) { + const auto done = [=](QString link) { *exporting = false; if (const auto onstack = finished) { - onstack(ok); + onstack(!link.isEmpty()); } }; ExportConferenceCallLink(uiShow(), _call->conferenceCall(), { @@ -998,6 +998,21 @@ Fn finished)> Panel::shareConferenceLinkCallback() { }; } +void Panel::migrationShowShareLink() { + ShowConferenceCallLinkBox( + uiShow(), + _call->conferenceCall(), + _call->existingConferenceLink(), + { .st = DarkConferenceCallLinkStyle() }); +} + +void Panel::migrationInviteUsers(std::vector> users) { + const auto done = [=](GroupCall::InviteResult result) { + showToast({ ComposeInviteResultToast(result) }); + }; + _call->inviteUsers(std::move(users), crl::guard(this, done)); +} + void Panel::enlargeVideo() { _lastSmallGeometry = window()->geometry(); diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index 05c399dea6..2af708af80 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -115,6 +115,9 @@ public: void hideLayer(anim::type animated = anim::type::normal); [[nodiscard]] bool isLayerShown() const; + void migrationShowShareLink(); + void migrationInviteUsers(std::vector> users); + void minimize(); void toggleFullScreen(); void close(); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 8fbb14c731..68200532f5 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1342,7 +1342,7 @@ peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; stats.messageStats#7fe91c14 views_graph:StatsGraph reactions_by_emotion_graph:StatsGraph = stats.MessageStats; groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall; -groupCall#d597650c flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true record_video_active:flags.11?true rtmp_stream:flags.12?true listeners_hidden:flags.13?true conference:flags.14?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int = GroupCall; +groupCall#d597650c flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true record_video_active:flags.11?true rtmp_stream:flags.12?true listeners_hidden:flags.13?true conference:flags.14?true creator:flags.15?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int = GroupCall; inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; inputGroupCallSlug#fe06823f slug:string = InputGroupCall; diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 9620615dd8..189adccc03 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/event_filter.h" #include "base/qt_signal_producer.h" -#include "base/random.h" #include "boxes/about_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/premium_preview_box.h" @@ -123,40 +122,23 @@ constexpr auto kPlayStatusLimit = 2; (height - st::inviteViaLinkIcon.height()) / 2); }, icon->lifetime()); - const auto creating = std::make_shared(); + const auto creating = std::make_shared(); result->setClickedCallback([=] { if (*creating) { return; } - *creating = base::RandomValue(); - const auto show = controller->uiShow(); - const auto session = &controller->session(); - session->api().request(MTPphone_CreateConferenceCall( - MTP_int(*creating) - )).done(crl::guard(controller, [=](const MTPphone_GroupCall &result) { - result.data().vcall().match([&](const auto &data) { - const auto call = session->data().sharedConferenceCall( - data.vid().v, - data.vaccess_hash().v); - call->processFullCall(result); - const auto finished = [=](bool ok) { - if (!ok) { - *creating = 0; - } else if (const auto onstack = done) { - onstack(); - } - }; - const auto show = controller->uiShow(); - Calls::Group::ExportConferenceCallLink(show, call, { - .initial = true, - .finished = finished, - .weakWindow = controller, - }); - }); - })).fail(crl::guard(controller, [=](const MTP::Error &error) { - show->showToast(error.type()); - *creating = 0; - })).send(); + *creating = true; + const auto finished = [=](QString link) { + if (link.isEmpty()) { + *creating = false; + } else if (const auto onstack = done) { + onstack(); + } + }; + Calls::Group::MakeConferenceCall({ + .show = controller->uiShow(), + .finished = finished, + }); }); return result; } diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 648e498da9..980d77a19a 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -842,39 +842,20 @@ void SessionNavigation::resolveCollectible( void SessionNavigation::resolveConferenceCall( QString slug, - Fn finished, - bool skipConfirm) { - resolveConferenceCall( - std::move(slug), - 0, - std::move(finished), - skipConfirm); + Fn finished) { + resolveConferenceCall(std::move(slug), 0, std::move(finished)); } void SessionNavigation::resolveConferenceCall( MsgId inviteMsgId, - Fn finished, - bool skipConfirm) { - resolveConferenceCall({}, inviteMsgId, std::move(finished), skipConfirm); + Fn finished) { + resolveConferenceCall({}, inviteMsgId, std::move(finished)); } void SessionNavigation::resolveConferenceCall( QString slug, MsgId inviteMsgId, - Fn finished, - bool skipConfirm) { - // Accept tg://call?slug= links as well. - const auto parts1 = QStringView(slug).split('#'); - if (!parts1.isEmpty()) { - const auto parts2 = parts1.front().split('&'); - if (!parts2.isEmpty()) { - const auto parts3 = parts2.front().split(u"slug="_q); - if (parts3.size() > 1) { - slug = parts3.back().toString(); - } - } - } - + Fn finished) { _conferenceCallResolveFinished = std::move(finished); if (_conferenceCallSlug == slug && _conferenceCallInviteMsgId == inviteMsgId) { @@ -903,19 +884,12 @@ void SessionNavigation::resolveConferenceCall( const auto confirmed = std::make_shared(); const auto join = [=] { *confirmed = true; - Core::App().calls().startOrJoinConferenceCall(uiShow(), { + Core::App().calls().startOrJoinConferenceCall({ .call = call, .linkSlug = slug, .joinMessageId = inviteMsgId, }); }; - if (skipConfirm) { - join(); - if (finished) { - finished(true); - } - return; - } const auto box = uiShow()->show(Box( Calls::Group::ConferenceCallJoinConfirm, call, diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index c31772fa3d..ceb30f1bd3 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -266,12 +266,10 @@ public: Fn fail = nullptr); void resolveConferenceCall( QString slug, - Fn finished = nullptr, - bool skipConfirm = false); + Fn finished = nullptr); void resolveConferenceCall( MsgId inviteMsgId, - Fn finished = nullptr, - bool skipConfirm = false); + Fn finished = nullptr); base::weak_ptr showToast( Ui::Toast::Config &&config); @@ -301,8 +299,7 @@ private: void resolveConferenceCall( QString slug, MsgId inviteMsgId, - Fn finished, - bool skipConfirm); + Fn finished); void resolveDone( const MTPcontacts_ResolvedPeer &result,