PoC conference call creation.

This commit is contained in:
John Preston 2025-03-25 22:38:13 +05:00
parent 5e6c81a98e
commit c80c23e8e8
14 changed files with 363 additions and 38 deletions

View file

@ -1540,6 +1540,8 @@ PRIVATE
support/support_preload.h
support/support_templates.cpp
support/support_templates.h
tde2e/tde2e_integration.cpp
tde2e/tde2e_integration.h
ui/boxes/edit_invite_link_session.cpp
ui/boxes/edit_invite_link_session.h
ui/boxes/peer_qr_box.cpp

View file

@ -4911,6 +4911,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_confcall_join_title" = "Group Call";
"lng_confcall_join_text" = "You are invited to join a Telegram Call.";
"lng_confcall_join_button" = "Join Group Call";
"lng_confcall_create_link" = "Create Call Link";
"lng_confcall_create_link_description" = "You can create a link that will allow your friends on Telegram to join the call.";
"lng_no_mic_permission" = "Telegram needs microphone access so that you can make calls and record voice messages.";

View file

@ -303,14 +303,19 @@ void Updates::feedUpdateVector(
auto list = updates.v;
const auto hasGroupCallParticipantUpdates = ranges::contains(
list,
mtpc_updateGroupCallParticipants,
&MTPUpdate::type);
true,
[](const MTPUpdate &update) {
return update.type() == mtpc_updateGroupCallParticipants
|| update.type() == mtpc_updateGroupCallChainBlocks;
});
if (hasGroupCallParticipantUpdates) {
ranges::stable_sort(list, std::less<>(), [](const MTPUpdate &entry) {
if (entry.type() == mtpc_updateGroupCallParticipants) {
if (entry.type() == mtpc_updateGroupCallChainBlocks) {
return 0;
} else {
} else if (entry.type() == mtpc_updateGroupCallParticipants) {
return 1;
} else {
return 2;
}
});
} else if (policy == SkipUpdatePolicy::SkipExceptGroupCallParticipants) {
@ -324,7 +329,8 @@ void Updates::feedUpdateVector(
if ((policy == SkipUpdatePolicy::SkipMessageIds
&& type == mtpc_updateMessageID)
|| (policy == SkipUpdatePolicy::SkipExceptGroupCallParticipants
&& type != mtpc_updateGroupCallParticipants)) {
&& type != mtpc_updateGroupCallParticipants
&& type != mtpc_updateGroupCallChainBlocks)) {
continue;
}
feedUpdate(entry);
@ -954,7 +960,8 @@ void Updates::applyGroupCallParticipantUpdates(const MTPUpdates &updates) {
data.vupdates(),
SkipUpdatePolicy::SkipExceptGroupCallParticipants);
}, [&](const MTPDupdateShort &data) {
if (data.vupdate().type() == mtpc_updateGroupCallParticipants) {
if (data.vupdate().type() == mtpc_updateGroupCallParticipants
|| data.vupdate().type() == mtpc_updateGroupCallChainBlocks) {
feedUpdate(data.vupdate());
}
}, [](const auto &) {
@ -2110,6 +2117,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
case mtpc_updatePhoneCall:
case mtpc_updatePhoneCallSignalingData:
case mtpc_updateGroupCallParticipants:
case mtpc_updateGroupCallChainBlocks:
case mtpc_updateGroupCallConnection:
case mtpc_updateGroupCall: {
Core::App().calls().handleUpdate(&session(), update);

View file

@ -235,16 +235,18 @@ void Instance::startOrJoinConferenceCall(
StartConferenceCallArgs args) {
destroyCurrentCall();
const auto session = &args.call->peer()->session();
auto call = std::make_unique<GroupCall>(
_delegate.get(),
Calls::Group::ConferenceInfo{
.call = args.call,
.call = std::move(args.call),
.e2e = std::move(args.e2e),
.linkSlug = args.linkSlug,
.joinMessageId = args.joinMessageId,
});
const auto raw = call.get();
args.call->peer()->session().account().sessionChanges(
session->account().sessionChanges(
) | rpl::start_with_next([=] {
destroyGroupCall(raw);
}, raw->lifetime());
@ -547,6 +549,8 @@ void Instance::handleUpdate(
handleGroupCallUpdate(session, update);
}, [&](const MTPDupdateGroupCallParticipants &data) {
handleGroupCallUpdate(session, update);
}, [&](const MTPDupdateGroupCallChainBlocks &data) {
handleGroupCallUpdate(session, update);
}, [](const auto &) {
Unexpected("Update type in Calls::Instance::handleUpdate.");
});
@ -677,6 +681,12 @@ void Instance::handleGroupCallUpdate(
}, [](const MTPDinputGroupCallSlug &) -> CallId {
Unexpected("slug in Instance::handleGroupCallUpdate");
});
}, [](const MTPDupdateGroupCallChainBlocks &data) {
return data.vcall().match([&](const MTPDinputGroupCall &data) {
return data.vid().v;
}, [](const MTPDinputGroupCallSlug &) -> CallId {
Unexpected("slug in Instance::handleGroupCallUpdate");
});
}, [](const auto &) -> CallId {
Unexpected("Type in Instance::handleGroupCallUpdate.");
});

View file

@ -45,6 +45,10 @@ namespace tgcalls {
class VideoCaptureInterface;
} // namespace tgcalls
namespace TdE2E {
class Call;
} // namespace TdE2E
namespace Calls {
class Call;
@ -66,6 +70,7 @@ struct StartGroupCallArgs {
struct StartConferenceCallArgs {
std::shared_ptr<Data::GroupCall> call;
std::shared_ptr<TdE2E::Call> e2e;
QString linkSlug;
MsgId joinMessageId;
};

View file

@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/global_shortcuts.h"
#include "base/random.h"
#include "tde2e/tde2e_api.h"
#include "tde2e/tde2e_integration.h"
#include "webrtc/webrtc_video_track.h"
#include "webrtc/webrtc_create_adm.h"
#include "webrtc/webrtc_environment.h"
@ -53,6 +54,10 @@ constexpr auto kFixManualLargeVideoDuration = 5 * crl::time(1000);
constexpr auto kFixSpeakingLargeVideoDuration = 3 * crl::time(1000);
constexpr auto kFullAsMediumsCount = 4; // 1 Full is like 4 Mediums.
constexpr auto kMaxMediumQualities = 16; // 4 Fulls or 16 Mediums.
constexpr auto kShortPollChainBlocksTimeout = 5 * crl::time(1000);
constexpr auto kShortPollChainBlocksPerRequest = 50;
constexpr auto kSubChain0 = 0;
constexpr auto kSubChain1 = 1;
[[nodiscard]] const Data::GroupCallParticipant *LookupParticipant(
not_null<PeerData*> peer,
@ -603,7 +608,8 @@ GroupCall::GroupCall(
Group::ConferenceInfo conference,
const MTPInputGroupCall &inputCall)
: _delegate(delegate)
, _conferenceCall(conference.call)
, _conferenceCall(std::move(conference.call))
, _e2e(std::move(conference.e2e))
, _peer(join.peer)
, _history(_peer->owner().history(_peer))
, _api(&_peer->session().mtp())
@ -641,8 +647,15 @@ GroupCall::GroupCall(
, _rtmp(join.rtmp)
, _rtmpVolume(Group::kDefaultVolume) {
if (_conferenceCall) {
_e2eState = std::make_unique<TdE2E::CallState>(
TdE2E::CreateCallState());
if (!_e2e) {
_e2e = std::make_shared<TdE2E::Call>(
TdE2E::MakeUserId(_peer->session().user()));
}
for (auto i = 0; i != kSubChainsCount; ++i) {
_subchains[i].timer.setCallback([=] {
checkChainBlocksRequest(i);
});
}
}
_muted.value(
@ -1419,10 +1432,7 @@ void GroupCall::rejoin(not_null<PeerData*> as) {
? Flag::f_video_stopped
: Flag(0))
| (_conferenceJoinMessageId ? Flag::f_invite_msg_id : Flag())
| (_e2eState ? Flag::f_public_key : Flag());
const auto publicKey = _e2eState
? _e2eState->myKey
: TdE2E::PublicKey();
| (_e2e ? Flag::f_public_key : Flag());
_api.request(MTPphone_JoinGroupCall(
MTP_flags(flags),
(_conferenceLinkSlug.isEmpty()
@ -1431,9 +1441,7 @@ void GroupCall::rejoin(not_null<PeerData*> as) {
MTP_string(_conferenceLinkSlug))),
joinAs()->input,
MTP_string(_joinHash),
MTP_int256(
MTP_int128(publicKey.a, publicKey.b),
MTP_int128(publicKey.c, publicKey.d)),
(_e2e ? TdE2E::PublicKeyToMTP(_e2e->myKey()) : MTPint256()),
MTP_int(_conferenceJoinMessageId.bare),
MTP_dataJSON(MTP_bytes(json))
)).done([=](
@ -1470,6 +1478,8 @@ void GroupCall::rejoin(not_null<PeerData*> as) {
real->reloadIfStale();
}
}
checkChainBlocksRequest(kSubChain0);
checkChainBlocksRequest(kSubChain1);
}).fail([=](const MTP::Error &error) {
_joinState.finish();
@ -1490,6 +1500,40 @@ void GroupCall::rejoin(not_null<PeerData*> as) {
});
}
void GroupCall::checkChainBlocksRequest(int subchain) {
Expects(subchain >= 0 && subchain < kSubChainsCount);
auto &state = _subchains[subchain];
if (state.requestId) {
return;
}
const auto now = crl::now();
const auto left = state.lastUpdate + kShortPollChainBlocksTimeout - now;
if (left > 0) {
if (!state.timer.isActive()) {
state.timer.callOnce(left);
}
return;
}
state.requestId = _api.request(MTPphone_GetGroupCallChainBlocks(
inputCall(),
MTP_int(subchain),
MTP_int(state.height),
MTP_int(kShortPollChainBlocksPerRequest)
)).done([=](const MTPUpdates &result) {
auto &state = _subchains[subchain];
state.lastUpdate = crl::now();
state.requestId = 0;
_peer->session().api().applyUpdates(result);
state.timer.callOnce(kShortPollChainBlocksTimeout + 1);
}).fail([=](const MTP::Error &error) {
auto &state = _subchains[subchain];
state.lastUpdate = crl::now();
state.requestId = 0;
state.timer.callOnce(kShortPollChainBlocksTimeout + 1);
}).send();
}
void GroupCall::checkNextJoinAction() {
if (_joinState.action != JoinAction::None) {
return;
@ -1640,7 +1684,6 @@ void GroupCall::applyMeInCallLocally() {
: participant
? participant->raisedHandRating
: FindLocalRaisedHandRating(real->participants());
const auto publicKey = _e2eState ? _e2eState->myKey : TdE2E::PublicKey();
const auto flags = (canSelfUnmute ? Flag::f_can_self_unmute : Flag(0))
| (lastActive ? Flag::f_active_date : Flag(0))
| (_joinState.ssrc ? Flag(0) : Flag::f_left)
@ -1650,7 +1693,7 @@ void GroupCall::applyMeInCallLocally() {
| Flag::f_volume_by_admin // Self volume can only be set by admin.
| ((muted() != MuteState::Active) ? Flag::f_muted : Flag(0))
| (raisedHandRating > 0 ? Flag::f_raise_hand_rating : Flag(0))
| (_e2eState ? Flag::f_public_key : Flag(0));
| (_e2e ? Flag::f_public_key : Flag(0));
real->applyLocalUpdate(
MTP_updateGroupCallParticipants(
inputCall(),
@ -1667,9 +1710,9 @@ void GroupCall::applyMeInCallLocally() {
MTP_long(raisedHandRating),
MTPGroupCallParticipantVideo(),
MTPGroupCallParticipantVideo(),
MTP_int256(
MTP_int128(publicKey.a, publicKey.b),
MTP_int128(publicKey.c, publicKey.d)))),
(_e2e
? TdE2E::PublicKeyToMTP(_e2e->myKey())
: MTPint256()))),
MTP_int(0)).c_updateGroupCallParticipants());
}
@ -2022,6 +2065,8 @@ void GroupCall::handleUpdate(const MTPUpdate &update) {
handleUpdate(data);
}, [&](const MTPDupdateGroupCallParticipants &data) {
handleUpdate(data);
}, [&](const MTPDupdateGroupCallChainBlocks &data) {
handleUpdate(data);
}, [](const auto &) {
Unexpected("Type in Instance::applyGroupCallUpdateChecked.");
});
@ -2063,6 +2108,33 @@ void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) {
}
}
void GroupCall::handleUpdate(const MTPDupdateGroupCallChainBlocks &data) {
const auto callId = data.vcall().match([](
const MTPDinputGroupCall &data) {
return data.vid().v;
}, [](const MTPDinputGroupCallSlug &) -> CallId {
Unexpected("inputGroupCallSlug in GroupCall::handleUpdate.");
});
if (_id != callId || !_e2e) {
return;
}
const auto subchain = data.vsub_chain_id().v;
if (subchain < 0 || subchain >= kSubChainsCount) {
return;
}
auto &entry = _subchains[subchain];
entry.lastUpdate = crl::now();
entry.height = data.vnext_offset().v;
entry.timer.callOnce(kShortPollChainBlocksTimeout + 1);
for (const auto &block : data.vblocks().v) {
const auto result = _e2e->apply({ block.v });
if (result == TdE2E::Call::ApplyResult::BlockSkipped) {
AssertIsDebug();
return;
}
}
}
void GroupCall::applyQueuedSelfUpdates() {
const auto weak = base::make_weak(this);
while (weak

View file

@ -44,7 +44,7 @@ class GroupCall;
namespace TdE2E {
struct ParticipantState;
struct CallState;
class Call;
} // namespace TdE2E
namespace Calls {
@ -437,6 +437,7 @@ private:
struct SinkPointer;
static constexpr uint32 kDisabledSsrc = uint32(-1);
static constexpr int kSubChainsCount = 2;
struct LoadingPart {
std::shared_ptr<LoadPartTask> task;
@ -475,6 +476,12 @@ private:
ssrc = updatedSsrc;
}
};
struct SubChainState {
crl::time lastUpdate = 0;
base::Timer timer;
int height = 0;
mtpRequestId requestId = 0;
};
friend inline constexpr bool is_flag_type(SendUpdateType) {
return true;
@ -507,6 +514,7 @@ private:
void handlePossibleDiscarded(const MTPDgroupCallDiscarded &data);
void handleUpdate(const MTPDupdateGroupCall &data);
void handleUpdate(const MTPDupdateGroupCallParticipants &data);
void handleUpdate(const MTPDupdateGroupCallChainBlocks &data);
bool tryCreateController();
void destroyController();
bool tryCreateScreencast();
@ -535,6 +543,7 @@ private:
void rejoinPresentation();
void leavePresentation();
void checkNextJoinAction();
void checkChainBlocksRequest(int subchain);
void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);
void setInstanceConnected(tgcalls::GroupNetworkState networkState);
@ -593,6 +602,8 @@ private:
const not_null<Delegate*> _delegate;
const std::shared_ptr<Data::GroupCall> _conferenceCall;
std::shared_ptr<TdE2E::Call> _e2e;
not_null<PeerData*> _peer; // Can change in legacy group migration.
rpl::event_stream<PeerData*> _peerStream;
not_null<History*> _history; // Can change in legacy group migration.
@ -624,8 +635,6 @@ private:
int64 _serverTimeMs = 0;
crl::time _serverTimeMsGotAt = 0;
std::unique_ptr<TdE2E::CallState> _e2eState;
QString _rtmpUrl;
QString _rtmpKey;
@ -710,6 +719,8 @@ private:
bool _reloadedStaleCall = false;
int _rtmpVolume = 0;
SubChainState _subchains[kSubChainsCount];
rpl::lifetime _lifetime;
};

View file

@ -19,6 +19,10 @@ namespace Ui {
class GenericBox;
} // namespace Ui
namespace TdE2E {
class Call;
} // namespace TdE2E
namespace Calls::Group {
constexpr auto kDefaultVolume = 10000;
@ -67,6 +71,7 @@ struct JoinInfo {
struct ConferenceInfo {
std::shared_ptr<Data::GroupCall> call;
std::shared_ptr<TdE2E::Call> e2e;
QString linkSlug;
MsgId joinMessageId;
};

View file

@ -19,6 +19,7 @@ struct ParticipantVideoParams;
namespace TdE2E {
struct ParticipantState;
struct UserId;
} // namespace TdE2E
namespace Data {

View file

@ -12,20 +12,77 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <tde2e/td/e2e/e2e_api.h>
namespace TdE2E {
namespace {
CallState CreateCallState() {
constexpr auto kPermissionAdd = 1;
constexpr auto kPermissionRemove = 2;
[[nodiscard]] tde2e_api::Slice Slice(const QByteArray &data) {
return {
data.constData(),
std::string_view::size_type(data.size())
};
}
} // namespace
Call::Call(UserId myUserId)
: _myUserId(myUserId) {
const auto id = tde2e_api::key_generate_temporary_private_key();
Assert(id.is_ok());
const auto key = tde2e_api::key_to_public_key(id.value());
_myKeyId = { .v = uint64(id.value()) };
const auto key = tde2e_api::key_to_public_key(_myKeyId.v);
Assert(key.is_ok());
Assert(key.value().size() == sizeof(_myKey));
memcpy(&_myKey, key.value().data(), sizeof(_myKey));
}
auto result = CallState{
.myKeyId = PrivateKeyId{ .v = uint64(id.value()) },
PublicKey Call::myKey() const {
return _myKey;
}
Block Call::makeZeroBlock() const {
const auto publicKeyView = std::string_view{
reinterpret_cast<const char*>(&_myKey),
sizeof(_myKey),
};
Assert(key.value().size() == sizeof(result.myKey));
memcpy(&result.myKey, key.value().data(), 32);
const auto publicKeyId = tde2e_api::key_from_public_key(publicKeyView);
Assert(publicKeyId.is_ok());
return result;
const auto myKeyId = std::int64_t(_myKeyId.v);
const auto result = tde2e_api::call_create_zero_block(myKeyId, {
.height = 0,
.participants = { {
.user_id = std::int64_t(_myUserId.v),
.public_key_id = publicKeyId.value(),
.permissions = kPermissionAdd | kPermissionRemove,
} },
});
Assert(result.is_ok());
return {
.data = QByteArray::fromStdString(result.value()),
};
}
void Call::create(const Block &last) {
tde2e_api::call_create(std::int64_t(_myKeyId.v), Slice(last.data));
}
Call::ApplyResult Call::apply(const Block &block) {
const auto result = tde2e_api::call_apply_block(
std::int64_t(_id.v),
Slice(block.data));
if (!result.is_ok()) {
const auto error = result.error();
(void)error;
}
return result.is_ok()
? ApplyResult::Success
: ApplyResult::BlockSkipped;
}
} // namespace TdE2E

View file

@ -11,10 +11,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace TdE2E {
struct UserId {
uint64 v = 0;
};
struct PrivateKeyId {
uint64 v = 0;
};
struct CallId {
uint64 v = 0;
};
struct PublicKey {
uint64 a = 0;
uint64 b = 0;
@ -23,15 +31,36 @@ struct PublicKey {
};
struct ParticipantState {
uint64 id = 0;
UserId id;
PublicKey key;
};
struct CallState {
PrivateKeyId myKeyId;
PublicKey myKey;
struct Block {
QByteArray data;
};
[[nodiscard]] CallState CreateCallState();
class Call final {
public:
explicit Call(UserId myUserId);
[[nodiscard]] PublicKey myKey() const;
[[nodiscard]] Block makeZeroBlock() const;
void create(const Block &last);
enum class ApplyResult {
Success,
BlockSkipped
};
[[nodiscard]] ApplyResult apply(const Block &block);
private:
CallId _id;
UserId _myUserId;
PrivateKeyId _myKeyId;
PublicKey _myKey;
};
} // namespace TdE2E

View file

@ -0,0 +1,27 @@
/*
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 "tde2e/tde2e_integration.h"
#include "data/data_user.h"
#include "tde2e/tde2e_api.h"
namespace TdE2E {
UserId MakeUserId(not_null<UserData*> user) {
return MakeUserId(peerToUser(user->id));
}
UserId MakeUserId(::UserId id) {
return { .v = id.bare };
}
MTPint256 PublicKeyToMTP(const PublicKey &key) {
return MTP_int256(MTP_int128(key.a, key.b), MTP_int128(key.c, key.d));
}
} // namespace TdE2E

View file

@ -0,0 +1,20 @@
/*
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
namespace TdE2E {
struct UserId;
struct PublicKey;
[[nodiscard]] UserId MakeUserId(not_null<UserData*> user);
[[nodiscard]] UserId MakeUserId(::UserId id);
[[nodiscard]] MTPint256 PublicKeyToMTP(const PublicKey &key);
} // namespace TdE2E

View file

@ -14,17 +14,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peer_list_controllers.h"
#include "boxes/premium_preview_box.h"
#include "calls/calls_box_controller.h"
#include "calls/calls_instance.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "data/data_changes.h"
#include "data/data_document_media.h"
#include "data/data_folder.h"
#include "data/data_group_call.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_user.h"
#include "info/info_memento.h"
#include "info/profile/info_profile_badge.h"
#include "info/profile/info_profile_emoji_status_panel.h"
#include "info/profile/info_profile_icon.h"
#include "info/stories/info_stories_widget.h"
#include "lang/lang_keys.h"
#include "main/main_account.h"
@ -38,6 +41,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "storage/localstorage.h"
#include "storage/storage_account.h"
#include "support/support_templates.h"
#include "tde2e/tde2e_api.h"
#include "tde2e/tde2e_integration.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_theme.h"
#include "ui/controls/swipe_handler.h"
@ -92,6 +97,66 @@ constexpr auto kPlayStatusLimit = 2;
|| (now.month() == 1 && now.day() == 1);
}
[[nodiscard]] not_null<Ui::SettingsButton*> AddCreateCallLinkButton(
not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
Fn<void()> done) {
const auto result = container->add(object_ptr<Ui::SettingsButton>(
container,
tr::lng_confcall_create_link(),
st::inviteViaLinkButton), QMargins());
Ui::AddSkip(container);
Ui::AddDividerText(
container,
tr::lng_confcall_create_link_description(Ui::Text::WithEntities));
const auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(
result,
st::inviteViaLinkIcon,
QPoint());
result->heightValue(
) | rpl::start_with_next([=](int height) {
icon->moveToLeft(
st::inviteViaLinkIconPosition.x(),
(height - st::inviteViaLinkIcon.height()) / 2);
}, icon->lifetime());
const auto creating = result->lifetime().make_state<bool>();
result->setClickedCallback([=] {
if (*creating) {
return;
}
*creating = true;
auto e2e = std::make_shared<TdE2E::Call>(
TdE2E::MakeUserId(controller->session().user()));
const auto session = &controller->session();
session->api().request(MTPphone_CreateConferenceCall(
TdE2E::PublicKeyToMTP(e2e->myKey()),
MTP_bytes(e2e->makeZeroBlock().data)
)).done(crl::guard(controller, [=](const MTPphone_GroupCall &result) {
result.data().vcall().match([&](const auto &data) {
const auto call = std::make_shared<Data::GroupCall>(
session->user(),
data.vid().v,
data.vaccess_hash().v,
TimeId(), // scheduleDate
false); // rtmp
call->processFullCall(result);
Core::App().calls().startOrJoinConferenceCall(
controller->uiShow(),
{ .call = call, .e2e = e2e });
});
if (const auto onstack = done) {
onstack();
}
})).fail(crl::guard(controller, [=](const MTP::Error &error) {
controller->uiShow()->showToast(error.type());
*creating = false;
})).send();
});
return result;
}
void ShowCallsBox(not_null<Window::SessionController*> window) {
struct State {
State(not_null<Window::SessionController*> window)
@ -129,6 +194,17 @@ void ShowCallsBox(not_null<Window::SessionController*> window) {
Ui::AddDivider(groupCalls->entity());
Ui::AddSkip(groupCalls->entity());
const auto button = AddCreateCallLinkButton(
box->verticalLayout(),
window,
crl::guard(box, [=] { box->closeBox(); }));
button->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::Enter);
}) | rpl::start_with_next([=] {
state->callsDelegate.peerListMouseLeftGeometry();
}, button->lifetime());
const auto content = box->addRow(
object_ptr<PeerListContent>(box, &state->callsController),
{});