From a3ba99c682fb186b67805d8a6bce17eb6e51759d Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 31 Mar 2025 11:44:39 +0400 Subject: [PATCH] Implement incoming confcall window. --- Telegram/SourceFiles/calls/calls_call.cpp | 109 +++++++++++++- Telegram/SourceFiles/calls/calls_call.h | 18 +++ Telegram/SourceFiles/calls/calls_instance.cpp | 133 +++++++++++++++++- Telegram/SourceFiles/calls/calls_instance.h | 28 ++++ .../calls/group/calls_group_members.cpp | 9 +- Telegram/SourceFiles/data/data_group_call.cpp | 11 +- .../SourceFiles/data/data_media_types.cpp | 26 +++- Telegram/SourceFiles/data/data_session.cpp | 1 + Telegram/SourceFiles/data/data_session.h | 1 + Telegram/SourceFiles/history/history.cpp | 5 +- .../window/window_session_controller.cpp | 11 +- 11 files changed, 327 insertions(+), 25 deletions(-) diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 2ff549c93d..02f3516871 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -247,7 +247,49 @@ Call::Call( setupOutgoingVideo(); } +Call::Call( + not_null delegate, + not_null user, + CallId conferenceId, + MsgId conferenceInviteMsgId) +: _delegate(delegate) +, _user(user) +, _api(&_user->session().mtp()) +, _type(Type::Incoming) +, _state(State::WaitingIncoming) +, _discardByTimeoutTimer([=] { hangup(); }) +, _playbackDeviceId( + &Core::App().mediaDevices(), + Webrtc::DeviceType::Playback, + Webrtc::DeviceIdValueWithFallback( + Core::App().settings().callPlaybackDeviceIdValue(), + Core::App().settings().playbackDeviceIdValue())) +, _captureDeviceId( + &Core::App().mediaDevices(), + Webrtc::DeviceType::Capture, + Webrtc::DeviceIdValueWithFallback( + Core::App().settings().callCaptureDeviceIdValue(), + Core::App().settings().captureDeviceIdValue())) +, _cameraDeviceId( + &Core::App().mediaDevices(), + Webrtc::DeviceType::Camera, + Core::App().settings().cameraDeviceIdValue()) +, _id(base::RandomValue()) +, _conferenceId(conferenceId) +, _conferenceInviteMsgId(conferenceInviteMsgId) +, _videoIncoming( + std::make_unique( + StartVideoState(false))) +, _videoOutgoing( + std::make_unique( + StartVideoState(false))) { + startWaitingTrack(); + setupOutgoingVideo(); +} + void Call::generateModExpFirst(bytes::const_span randomSeed) { + Expects(!conferenceInvite()); + auto first = MTP::CreateModExp(_dhConfig.g, _dhConfig.p, randomSeed); if (first.modexp.empty()) { LOG(("Call Error: Could not compute mod-exp first.")); @@ -273,6 +315,8 @@ bool Call::isIncomingWaiting() const { } void Call::start(bytes::const_span random) { + Expects(!conferenceInvite()); + // Save config here, because it is possible that it changes between // different usages inside the same call. _dhConfig = _delegate->getDhConfig(); @@ -297,6 +341,7 @@ void Call::startOutgoing() { Expects(_type == Type::Outgoing); Expects(_state.current() == State::Requesting); Expects(_gaHash.size() == kSha256Size); + Expects(!conferenceInvite()); const auto flags = _videoCapture ? MTPphone_RequestCall::Flag::f_video @@ -350,6 +395,7 @@ void Call::startOutgoing() { void Call::startIncoming() { Expects(_type == Type::Incoming); Expects(_state.current() == State::Starting); + Expects(!conferenceInvite()); _api.request(MTPphone_ReceivedCall( MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)) @@ -363,6 +409,8 @@ void Call::startIncoming() { } void Call::applyUserConfirmation() { + Expects(!conferenceInvite()); + if (_state.current() == State::WaitingUserConfirmation) { setState(State::Requesting); } @@ -375,9 +423,44 @@ void Call::answer() { }), video); } +void Call::acceptConferenceInvite() { + Expects(conferenceInvite()); + + if (_state.current() != State::WaitingIncoming) { + return; + } + setState(State::ExchangingKeys); + const auto limit = 5; + const auto messageId = _conferenceInviteMsgId; + const auto session = &_user->session(); + session->api().request(MTPphone_GetGroupCall( + MTP_inputGroupCallInviteMessage(MTP_int(messageId.bare)), + MTP_int(limit) + )).done([session, messageId](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, + .joinMessageId = messageId, + .migrating = true, + }); + }); + }).fail(crl::guard(this, [=](const MTP::Error &error) { + handleRequestError(error.type()); + })).send(); +} + void Call::actuallyAnswer() { Expects(_type == Type::Incoming); + if (conferenceInvite()) { + acceptConferenceInvite(); + return; + } + const auto state = _state.current(); if (state != State::Starting && state != State::WaitingIncoming) { if (state != State::ExchangingKeys @@ -435,6 +518,8 @@ void Call::setMuted(bool mute) { } void Call::setupMediaDevices() { + Expects(!conferenceInvite()); + _playbackDeviceId.changes() | rpl::filter([=] { return _instance && _setDeviceIdCallback; }) | rpl::start_with_next([=](const Webrtc::DeviceResolvedId &deviceId) { @@ -530,7 +615,8 @@ crl::time Call::getDurationMs() const { void Call::hangup(Data::GroupCall *migrateCall, const QString &migrateSlug) { const auto state = _state.current(); - if (state == State::Busy || state == State::MigrationHangingUp) { + if (state == State::Busy + || state == State::MigrationHangingUp) { _delegate->callFinished(this); } else { const auto missed = (state == State::Ringing @@ -549,6 +635,8 @@ void Call::hangup(Data::GroupCall *migrateCall, const QString &migrateSlug) { } void Call::redial() { + Expects(!conferenceInvite()); + if (_state.current() != State::Busy) { return; } @@ -578,6 +666,8 @@ void Call::startWaitingTrack() { } void Call::sendSignalingData(const QByteArray &data) { + Expects(!conferenceInvite()); + _api.request(MTPphone_SendSignalingData( MTP_inputPhoneCall( MTP_long(_id), @@ -776,6 +866,8 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { } void Call::finishByMigration(const QString &slug) { + Expects(!conferenceInvite()); + if (_state.current() == State::MigrationHangingUp) { return; } @@ -841,6 +933,7 @@ bool Call::handleSignalingData( void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) { Expects(_type == Type::Outgoing); + Expects(!conferenceInvite()); if (_state.current() == State::ExchangingKeys || _instance) { @@ -893,6 +986,7 @@ void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) { void Call::startConfirmedCall(const MTPDphoneCall &call) { Expects(_type == Type::Incoming); + Expects(!conferenceInvite()); const auto firstBytes = bytes::make_span(call.vg_a_or_b().v); if (_gaHash != openssl::Sha256(firstBytes)) { @@ -919,6 +1013,8 @@ void Call::startConfirmedCall(const MTPDphoneCall &call) { } void Call::createAndStartController(const MTPDphoneCall &call) { + Expects(!conferenceInvite()); + _discardByTimeoutTimer.cancel(); if (!checkCallFields(call) || _authKey.size() != kAuthKeySize) { return; @@ -1116,6 +1212,8 @@ void Call::createAndStartController(const MTPDphoneCall &call) { } void Call::handleControllerStateChange(tgcalls::State state) { + Expects(!conferenceInvite()); + switch (state) { case tgcalls::State::WaitInit: { DEBUG_LOG(("Call Info: State changed to WaitingInit.")); @@ -1390,8 +1488,11 @@ void Call::finish( || state == State::Ended || state == State::Failed) { return; - } - if (!_id) { + } else if (conferenceInvite()) { + Core::App().calls().declineIncomingConferenceInvites(_conferenceId); + setState(finalState); + return; + } else if (!_id) { setState(finalState); return; } @@ -1460,7 +1561,7 @@ void Call::handleRequestError(const QString &error) { ? Lang::Hard::CallErrorIncompatible().replace( "{user}", _user->name()) - : QString(); + : error; if (!inform.isEmpty()) { if (const auto window = Core::App().windowFor( Window::SeparateId(_user))) { diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index 2e024cea83..6be26dd259 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -102,6 +102,11 @@ public: not_null user, Type type, bool video); + Call( + not_null delegate, + not_null user, + CallId conferenceId, + MsgId conferenceInviteMsgId); [[nodiscard]] Type type() const { return _type; @@ -112,6 +117,15 @@ public: [[nodiscard]] CallId id() const { return _id; } + [[nodiscard]] bool conferenceInvite() const { + return _conferenceId != 0; + } + [[nodiscard]] CallId conferenceId() const { + return _conferenceId; + } + [[nodiscard]] MsgId conferenceInviteMsgId() const { + return _conferenceInviteMsgId; + } [[nodiscard]] bool isIncomingWaiting() const; void start(bytes::const_span random); @@ -272,6 +286,7 @@ private: bool checkCallFields(const MTPDphoneCallAccepted &call); void actuallyAnswer(); + void acceptConferenceInvite(); void confirmAcceptedCall(const MTPDphoneCallAccepted &call); void startConfirmedCall(const MTPDphoneCall &call); void setState(State state); @@ -325,6 +340,9 @@ private: uint64 _accessHash = 0; uint64 _keyFingerprint = 0; + CallId _conferenceId = 0; + MsgId _conferenceInviteMsgId = 0; + std::unique_ptr _instance; std::shared_ptr _videoCapture; QString _videoCaptureDeviceId; diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 833b40e93f..0e122b7c4c 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/group/calls_choose_join_as.h" #include "calls/group/calls_group_call.h" #include "calls/group/calls_group_rtmp.h" +#include "history/history.h" +#include "history/history_item.h" #include "mtproto/mtproto_dh_utils.h" #include "core/application.h" #include "core/core_settings.h" @@ -26,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_panel.h" #include "data/data_user.h" #include "data/data_group_call.h" +#include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_session.h" @@ -103,6 +106,8 @@ void Instance::Delegate::callFailed(not_null call) { } void Instance::Delegate::callRedial(not_null call) { + Expects(!call->conferenceInvite()); + if (_instance->_currentCall.get() == call) { _instance->refreshDhConfig(); } @@ -256,7 +261,7 @@ void Instance::startOrJoinConferenceCall(StartConferenceCallArgs args) { _currentGroupCallChanges.fire_copy(raw); if (!args.invite.empty()) { _currentGroupCallPanel->migrationInviteUsers(std::move(args.invite)); - } else if (args.migrating) { + } else if (args.migrating && !args.linkSlug.isEmpty()) { _currentGroupCallPanel->migrationShowShareLink(); } } @@ -442,6 +447,7 @@ void Instance::createGroupCall( void Instance::refreshDhConfig() { Expects(_currentCall != nullptr); + Expects(!_currentCall->conferenceInvite()); const auto weak = base::make_weak(_currentCall); _currentCall->user()->session().api().request(MTPmessages_GetDhConfig( @@ -899,4 +905,129 @@ std::shared_ptr Instance::getVideoCapture( return result; } +const ConferenceInvites &Instance::conferenceInvites( + CallId conferenceId) const { + static const auto kEmpty = ConferenceInvites(); + const auto i = _conferenceInvites.find(conferenceId); + return (i != end(_conferenceInvites)) ? i->second : kEmpty; +} + +void Instance::registerConferenceInvite( + CallId conferenceId, + not_null user, + MsgId messageId, + bool incoming) { + auto &info = _conferenceInvites[conferenceId].users[user]; + (incoming ? info.incoming : info.outgoing).emplace(messageId); +} + +void Instance::unregisterConferenceInvite( + CallId conferenceId, + not_null user, + MsgId messageId, + bool incoming) { + const auto i = _conferenceInvites.find(conferenceId); + if (i == end(_conferenceInvites)) { + return; + } + const auto j = i->second.users.find(user); + if (j == end(i->second.users)) { + return; + } + auto &info = j->second; + (incoming ? info.incoming : info.outgoing).remove(messageId); + if (!incoming) { + user->owner().unregisterInvitedToCallUser(conferenceId, user); + } + if (info.incoming.empty() && info.outgoing.empty()) { + i->second.users.erase(j); + if (i->second.users.empty()) { + _conferenceInvites.erase(i); + } + } + if (_currentCall + && _currentCall->user() == user + && _currentCall->conferenceInviteMsgId() == messageId + && _currentCall->state() == Call::State::WaitingIncoming) { + destroyCurrentCall(); + } +} + +void Instance::declineIncomingConferenceInvites(CallId conferenceId) { + const auto i = _conferenceInvites.find(conferenceId); + if (i == end(_conferenceInvites)) { + return; + } + for (auto j = begin(i->second.users); j != end(i->second.users);) { + const auto api = &j->first->session().api(); + for (const auto &messageId : base::take(j->second.incoming)) { + api->request(MTPphone_DeclineConferenceCallInvite( + MTP_int(messageId.bare) + )).send(); + } + if (j->second.outgoing.empty()) { + j = i->second.users.erase(j); + } else { + ++j; + } + } + if (i->second.users.empty()) { + _conferenceInvites.erase(i); + } +} + +void Instance::showConferenceInvite( + not_null user, + MsgId conferenceInviteMsgId) { + const auto item = user->owner().message(user, conferenceInviteMsgId); + const auto media = item ? item->media() : nullptr; + const auto call = media ? media->call() : nullptr; + const auto conferenceId = call ? call->conferenceId : 0; + if (!conferenceId + || call->state != Data::CallState::Invitation + || user->isSelf()) { + return; + } else if (_currentCall + && _currentCall->conferenceId() == conferenceId) { + return; + } else if (inGroupCall() + && _currentGroupCall->conference() + && _currentGroupCall->conferenceCall()->id() == conferenceId) { + return; + } + + const auto &config = user->session().serverConfig(); + if (inCall() || inGroupCall()) { + declineIncomingConferenceInvites(conferenceId); + } else if (item->date() + (config.callRingTimeoutMs / 1000) + < base::unixtime::now()) { + declineIncomingConferenceInvites(conferenceId); + LOG(("Ignoring too old conference call invitation.")); + } else { + const auto delegate = _delegate.get(); + auto call = std::make_unique( + delegate, + user, + conferenceId, + conferenceInviteMsgId); + const auto raw = call.get(); + + user->session().account().sessionChanges( + ) | rpl::start_with_next([=] { + destroyCall(raw); + }, raw->lifetime()); + + if (_currentCall) { + _currentCallPanel->replaceCall(raw); + std::swap(_currentCall, call); + call->hangup(); + } else { + _currentCallPanel = std::make_unique(raw); + _currentCall = std::move(call); + } + _currentCallChanges.fire_copy(raw); + } +} + + } // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index ad55bb5dec..f2fb73ee96 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -77,6 +77,15 @@ struct StartConferenceCallArgs { bool migrating = false; }; +struct ConferenceInviteMessages { + base::flat_set incoming; + base::flat_set outgoing; +}; + +struct ConferenceInvites { + base::flat_map, ConferenceInviteMessages> users; +}; + class Instance final : public base::has_weak_ptr { public: Instance(); @@ -122,6 +131,23 @@ public: -> std::shared_ptr; void requestPermissionsOrFail(Fn onSuccess, bool video = true); + [[nodiscard]] const ConferenceInvites &conferenceInvites( + CallId conferenceId) const; + void registerConferenceInvite( + CallId conferenceId, + not_null user, + MsgId messageId, + bool incoming); + void unregisterConferenceInvite( + CallId conferenceId, + not_null user, + MsgId messageId, + bool incoming); + void showConferenceInvite( + not_null user, + MsgId conferenceInviteMsgId); + void declineIncomingConferenceInvites(CallId conferenceId); + [[nodiscard]] FnMut addAsyncWaiter(); [[nodiscard]] bool isSharingScreen() const; @@ -188,6 +214,8 @@ private: const std::unique_ptr _chooseJoinAs; const std::unique_ptr _startWithRtmp; + base::flat_map _conferenceInvites; + base::flat_set> _asyncWaiters; }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index 4d14d0f812..b1b1fa0c2b 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -513,7 +513,14 @@ void Members::Controller::setupInvitedUsers() { ) | rpl::filter([=](const Invite &invite) { return (invite.id == _call->id()); }) | rpl::start_with_next([=](const Invite &invite) { - if (auto row = createInvitedRow(invite.user)) { + if (invite.removed) { + if (const auto row = findRow(invite.user)) { + if (row->state() == Row::State::Invited) { + delegate()->peerListRemoveRow(row); + delegate()->peerListRefreshRows(); + } + } + } else if (auto row = createInvitedRow(invite.user)) { delegate()->peerListAppendRow(std::move(row)); delegate()->peerListRefreshRows(); } diff --git a/Telegram/SourceFiles/data/data_group_call.cpp b/Telegram/SourceFiles/data/data_group_call.cpp index dd6e449cbb..1dc6fef8ca 100644 --- a/Telegram/SourceFiles/data/data_group_call.cpp +++ b/Telegram/SourceFiles/data/data_group_call.cpp @@ -789,7 +789,8 @@ void GroupCall::applyParticipantsSlice( .videoJoined = videoJoined, .applyVolumeFromMin = applyVolumeFromMin, }; - if (i == end(_participants)) { + const auto adding = (i == end(_participants)); + if (adding) { if (value.ssrc) { _participantPeerByAudioSsrc.emplace( value.ssrc, @@ -802,9 +803,6 @@ void GroupCall::applyParticipantsSlice( participantPeer); } _participants.push_back(value); - if (const auto user = participantPeer->asUser()) { - _peer->owner().unregisterInvitedToCallUser(_id, user); - } } else { if (i->ssrc != value.ssrc) { _participantPeerByAudioSsrc.erase(i->ssrc); @@ -836,6 +834,11 @@ void GroupCall::applyParticipantsSlice( .now = value, }); } + if (adding) { + if (const auto user = participantPeer->asUser()) { + _peer->owner().unregisterInvitedToCallUser(_id, user); + } + } }); } if (sliceSource == ApplySliceSource::UpdateReceived) { diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index ab2c7e4712..8ef7f28c59 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "calls/calls_instance.h" #include "core/application.h" #include "core/click_handler_types.h" // ClickHandlerContext #include "lang/lang_keys.h" @@ -1684,11 +1685,32 @@ std::unique_ptr MediaLocation::createView( MediaCall::MediaCall(not_null parent, const Call &call) : Media(parent) , _call(call) { - parent->history()->owner().registerCallItem(parent); + const auto peer = parent->history()->peer; + peer->owner().registerCallItem(parent); + if (const auto user = _call.conferenceId ? peer->asUser() : nullptr) { + if (_call.state == CallState::Invitation) { + Core::App().calls().registerConferenceInvite( + _call.conferenceId, + user, + parent->id, + !parent->out()); + } + } } MediaCall::~MediaCall() { - parent()->history()->owner().unregisterCallItem(parent()); + const auto parent = this->parent(); + const auto peer = parent->history()->peer; + peer->owner().unregisterCallItem(parent); + if (const auto user = _call.conferenceId ? peer->asUser() : nullptr) { + if (_call.state == CallState::Invitation) { + Core::App().calls().unregisterConferenceInvite( + _call.conferenceId, + user, + parent->id, + !parent->out()); + } + } } std::unique_ptr MediaCall::clone(not_null parent) { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index fd205d0e03..641845ae0e 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -1257,6 +1257,7 @@ void Session::unregisterInvitedToCallUser( i->second.remove(user); if (i->second.empty()) { _invitedToCallUsers.erase(i); + _invitesToCalls.fire({ callId, user, true }); } } } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 011d98dd43..e1563d8563 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -256,6 +256,7 @@ public: struct InviteToCall { CallId id = 0; not_null user; + bool removed = false; }; [[nodiscard]] rpl::producer invitesToCalls() const { return _invitesToCalls.events(); diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 8284bd5315..35dc39acfc 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -52,7 +52,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "main/main_session.h" #include "window/notifications_manager.h" -#include "window/window_session_controller.h" #include "calls/calls_instance.h" #include "spellcheck/spellcheck_types.h" #include "storage/localstorage.h" @@ -1228,8 +1227,8 @@ void History::applyServiceChanges( } }, [&](const MTPDmessageActionConferenceCall &data) { if (!data.is_active() && !data.is_missed() && !item->out()) { - if (const auto window = session().tryResolveWindow()) { - window->resolveConferenceCall(item->id); + if (const auto user = item->history()->peer->asUser()) { + Core::App().calls().showConferenceInvite(user, item->id); } } }, [](const auto &) { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 980d77a19a..c689c9e9aa 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -881,26 +881,17 @@ void SessionNavigation::resolveConferenceCall( data.vid().v, data.vaccess_hash().v); call->processFullCall(result); - const auto confirmed = std::make_shared(); const auto join = [=] { - *confirmed = true; Core::App().calls().startOrJoinConferenceCall({ .call = call, .linkSlug = slug, .joinMessageId = inviteMsgId, }); }; - const auto box = uiShow()->show(Box( + uiShow()->show(Box( Calls::Group::ConferenceCallJoinConfirm, call, join)); - box->boxClosing() | rpl::start_with_next([=] { - if (inviteMsgId && !*confirmed) { - _api.request(MTPphone_DeclineConferenceCallInvite( - MTP_int(inviteMsgId.bare) - )).send(); - } - }, box->lifetime()); if (finished) { finished(true); }