Initial call->confcall migration.

This commit is contained in:
John Preston 2025-03-30 00:53:11 +05:00
parent 9dbd134601
commit d052eac019
20 changed files with 561 additions and 182 deletions

View file

@ -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";

View file

@ -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;

View file

@ -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<Webrtc::DeviceResolvedId> 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.

View file

@ -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();

View file

@ -230,12 +230,13 @@ void Instance::startOrJoinGroupCall(
});
}
void Instance::startOrJoinConferenceCall(
std::shared_ptr<Ui::Show> 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<GroupCall>(
_delegate.get(),
Calls::Group::ConferenceInfo{
@ -254,6 +255,11 @@ void Instance::startOrJoinConferenceCall(
_currentGroupCallPanel = std::make_unique<Group::Panel>(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<GroupCall>(
_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<Group::Panel>(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);
}

View file

@ -73,6 +73,8 @@ struct StartConferenceCallArgs {
std::shared_ptr<TdE2E::Call> e2e;
QString linkSlug;
MsgId joinMessageId;
std::vector<not_null<UserData*>> invite;
bool migrating = false;
};
class Instance final : public base::has_weak_ptr {
@ -85,9 +87,7 @@ public:
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
StartGroupCallArgs args);
void startOrJoinConferenceCall(
std::shared_ptr<Ui::Show> show,
StartConferenceCallArgs args);
void startOrJoinConferenceCall(StartConferenceCallArgs args);
void showStartWithRtmp(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer);
@ -140,7 +140,6 @@ private:
void createGroupCall(
Group::JoinInfo info,
const MTPInputGroupCall &inputCall);
void createConferenceCall(Group::ConferenceInfo info);
void destroyGroupCall(not_null<GroupCall*> call);
void confirmLeaveCurrent(
std::shared_ptr<Ui::Show> show,
@ -156,7 +155,9 @@ private:
void refreshServerConfig(not_null<Main::Session*> 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<Main::Session*> session,
const MTPPhoneCall &call);

View file

@ -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);
</svg>)";
}
class Show final : public Main::SessionShow {
public:
explicit Show(not_null<Panel*> panel);
~Show();
void showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<Ui::BoxContent>,
std::unique_ptr<Ui::LayerWidget>> &&layer,
Ui::LayerOptions options,
anim::type animated) const override;
[[nodiscard]] not_null<QWidget*> toastParent() const override;
[[nodiscard]] bool valid() const override;
operator bool() const override;
[[nodiscard]] Main::Session &session() const override;
private:
const base::weak_ptr<Panel> _panel;
};
Show::Show(not_null<Panel*> panel)
: _panel(base::make_weak(panel)) {
}
Show::~Show() = default;
void Show::showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<Ui::BoxContent>,
std::unique_ptr<Ui::LayerWidget>> &&layer,
Ui::LayerOptions options,
anim::type animated) const {
using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
using ObjectBox = object_ptr<Ui::BoxContent>;
if (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {
if (const auto panel = _panel.get()) {
panel->showLayer(std::move(*layerWidget), options, animated);
}
} else if (auto box = std::get_if<ObjectBox>(&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<QWidget*> 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*> call)
@ -121,6 +194,9 @@ Panel::Panel(not_null<Call*> call)
widget(),
st::callMicrophoneMute,
&st::callMicrophoneUnmute))
, _addPeople(
widget(),
object_ptr<Ui::CallButton>(widget(), st::callAddPeople))
, _name(widget(), st::callName)
, _status(widget(), st::callStatus)
, _hideControlsTimer([=] { requestControlsHidden(true); })
@ -133,6 +209,8 @@ Panel::Panel(not_null<Call*> 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<Ui::Toast::Instance> Panel::showToast(
const QString &text,
crl::time duration) {
return showToast({
.text = { text },
.duration = duration,
});
}
base::weak_ptr<Ui::Toast::Instance> Panel::showToast(
TextWithEntities &&text,
crl::time duration) {
return showToast({
.text = std::move(text),
.duration = duration,
});
}
base::weak_ptr<Ui::Toast::Instance> 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<Ui::BoxContent> box) {
showBox(std::move(box), Ui::LayerOption::KeepOther, anim::type::normal);
}
void Panel::showBox(
object_ptr<Ui::BoxContent> box,
Ui::LayerOptions options,
anim::type animated) {
_layerBg->showBox(std::move(box), options, animated);
}
void Panel::showLayer(
std::unique_ptr<Ui::LayerWidget> 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<Main::SessionShow> Panel::uiShow() {
return std::make_shared<Show>(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<bool>();
const auto finish = [=](QString link) {
if (link.isEmpty()) {
*creating = false;
}
};
const auto create = [=](std::vector<not_null<UserData*>> 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<not_null<UserData*>> 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::PowerSaveBlocker>(
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<Ui::RpWidget*> Panel::widget() const {
return _window.widget();
}
not_null<UserData*> 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();

View file

@ -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<LayerOption>;
class IconButton;
class CallButton;
class LayerManager;
@ -38,14 +46,17 @@ template <typename Widget>
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*> call);
~Panel();
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
[[nodiscard]] not_null<UserData*> user() const;
[[nodiscard]] bool isVisible() const;
[[nodiscard]] bool isActive() const;
base::weak_ptr<Ui::Toast::Instance> showToast(
const QString &text,
crl::time duration = 0);
base::weak_ptr<Ui::Toast::Instance> showToast(
TextWithEntities &&text,
crl::time duration = 0);
base::weak_ptr<Ui::Toast::Instance> showToast(
Ui::Toast::Config &&config);
void showBox(object_ptr<Ui::BoxContent> box);
void showBox(
object_ptr<Ui::BoxContent> box,
Ui::LayerOptions options,
anim::type animated = anim::type::normal);
void showLayer(
std::unique_ptr<Ui::LayerWidget> 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<bool> startOutgoingRequests() const;
[[nodiscard]] std::shared_ptr<Main::SessionShow> uiShow();
[[nodiscard]] rpl::lifetime &lifetime();
private:
@ -97,7 +136,6 @@ private:
};
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
void paint(QRect clip);
@ -170,6 +208,7 @@ private:
base::unique_qptr<Ui::CallButton> _startVideo;
object_ptr<Ui::FadeWrap<Ui::CallButton>> _mute;
Ui::CallButton *_audioDeviceToggle = nullptr;
object_ptr < Ui::FadeWrap<Ui::CallButton>> _addPeople;
object_ptr<Ui::FlatLabel> _name;
object_ptr<Ui::FlatLabel> _status;
object_ptr<Ui::RpWidget> _fingerprint = { nullptr };

View file

@ -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<Data::GroupCall> 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<not_null<Data::GroupCall*>> 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

View file

@ -257,6 +257,7 @@ public:
[[nodiscard]] Data::GroupCall *lookupReal() const;
[[nodiscard]] std::shared_ptr<Data::GroupCall> conferenceCall() const;
[[nodiscard]] QString existingConferenceLink() const;
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
[[nodiscard]] rpl::producer<QByteArray> emojiHashValue() const;
@ -752,4 +753,7 @@ private:
};
[[nodiscard]] TextWithEntities ComposeInviteResultToast(
const GroupCall::InviteResult &result);
} // namespace Calls

View file

@ -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 <QtGui/QClipboard>
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<Ui::GenericBox> 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<Ui::GenericBox*> 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<Data::GroupCall> 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<int32>())
)).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();
}

View file

@ -142,8 +142,10 @@ struct ConferenceCallLinkStyleOverrides {
struct ConferenceCallLinkArgs {
bool initial = false;
Fn<void(bool)> finished;
base::weak_ptr<Window::SessionController> weakWindow = nullptr;
bool joining = false;
bool migrating = false;
Fn<void(QString)> finished;
std::vector<not_null<UserData*>> invite;
ConferenceCallLinkStyleOverrides st;
};
void ShowConferenceCallLinkBox(
@ -157,4 +159,13 @@ void ExportConferenceCallLink(
std::shared_ptr<Data::GroupCall> call,
ConferenceCallLinkArgs &&args);
struct ConferenceFactoryArgs {
std::shared_ptr<Main::SessionShow> show;
Fn<void(QString)> finished;
std::vector<not_null<UserData*>> invite;
bool joining = false;
bool migrating = false;
};
void MakeConferenceCall(ConferenceFactoryArgs &&args);
} // namespace Calls::Group

View file

@ -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<not_null<UserData*>> _alreadyIn;
const base::flat_set<not_null<UserData*>> _alreadyIn;
const Fn<void()> _shareLink;
rpl::variable<bool> _hasSelected;
@ -93,7 +94,6 @@ ConfInviteController::ConfInviteController(
: ContactsBoxController(session)
, _alreadyIn(std::move(alreadyIn))
, _shareLink(std::move(shareLink)) {
_alreadyIn.remove(session->user());
}
rpl::producer<bool> 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<Ui::BoxContent> PrepareInviteBox(
return Box<PeerListsBox>(std::move(controllers), initBox);
}
object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<Call*> call,
Fn<void(std::vector<not_null<UserData*>>)> inviteUsers,
Fn<void()> shareLink) {
const auto user = call->user();
const auto weak = base::make_weak(call);
auto alreadyIn = base::flat_set<not_null<UserData*>>{ user };
auto controller = std::make_unique<ConfInviteController>(
&user->session(),
alreadyIn,
shareLink);
const auto raw = controller.get();
raw->setStyleOverrides(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
auto initBox = [=](not_null<PeerListBox*> 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<PeerData*> peer) {
return not_null(peer->asUser());
}) | ranges::to_vector;
inviteUsers(std::move(users));
});
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}, box->lifetime());
};
return Box<PeerListBox>(std::move(controller), initBox);
}
} // namespace Calls::Group

View file

@ -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<void(TextWithEntities&&)> showToast,
Fn<void(Fn<void(bool)> finished)> shareConferenceLink = nullptr);
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<Call*> call,
Fn<void(std::vector<not_null<UserData*>>)> inviteUsers,
Fn<void()> shareLink);
} // namespace Calls::Group

View file

@ -985,10 +985,10 @@ Fn<void(Fn<void(bool)> 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<void(Fn<void(bool)> finished)> Panel::shareConferenceLinkCallback() {
};
}
void Panel::migrationShowShareLink() {
ShowConferenceCallLinkBox(
uiShow(),
_call->conferenceCall(),
_call->existingConferenceLink(),
{ .st = DarkConferenceCallLinkStyle() });
}
void Panel::migrationInviteUsers(std::vector<not_null<UserData*>> 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();

View file

@ -115,6 +115,9 @@ public:
void hideLayer(anim::type animated = anim::type::normal);
[[nodiscard]] bool isLayerShown() const;
void migrationShowShareLink();
void migrationInviteUsers(std::vector<not_null<UserData*>> users);
void minimize();
void toggleFullScreen();
void close();

View file

@ -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;

View file

@ -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<int32>();
const auto creating = std::make_shared<bool>();
result->setClickedCallback([=] {
if (*creating) {
return;
}
*creating = base::RandomValue<int32>();
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;
}

View file

@ -842,39 +842,20 @@ void SessionNavigation::resolveCollectible(
void SessionNavigation::resolveConferenceCall(
QString slug,
Fn<void(bool)> finished,
bool skipConfirm) {
resolveConferenceCall(
std::move(slug),
0,
std::move(finished),
skipConfirm);
Fn<void(bool)> finished) {
resolveConferenceCall(std::move(slug), 0, std::move(finished));
}
void SessionNavigation::resolveConferenceCall(
MsgId inviteMsgId,
Fn<void(bool)> finished,
bool skipConfirm) {
resolveConferenceCall({}, inviteMsgId, std::move(finished), skipConfirm);
Fn<void(bool)> finished) {
resolveConferenceCall({}, inviteMsgId, std::move(finished));
}
void SessionNavigation::resolveConferenceCall(
QString slug,
MsgId inviteMsgId,
Fn<void(bool)> 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<void(bool)> finished) {
_conferenceCallResolveFinished = std::move(finished);
if (_conferenceCallSlug == slug
&& _conferenceCallInviteMsgId == inviteMsgId) {
@ -903,19 +884,12 @@ void SessionNavigation::resolveConferenceCall(
const auto confirmed = std::make_shared<bool>();
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,

View file

@ -266,12 +266,10 @@ public:
Fn<void(QString)> fail = nullptr);
void resolveConferenceCall(
QString slug,
Fn<void(bool)> finished = nullptr,
bool skipConfirm = false);
Fn<void(bool)> finished = nullptr);
void resolveConferenceCall(
MsgId inviteMsgId,
Fn<void(bool)> finished = nullptr,
bool skipConfirm = false);
Fn<void(bool)> finished = nullptr);
base::weak_ptr<Ui::Toast::Instance> showToast(
Ui::Toast::Config &&config);
@ -301,8 +299,7 @@ private:
void resolveConferenceCall(
QString slug,
MsgId inviteMsgId,
Fn<void(bool)> finished,
bool skipConfirm);
Fn<void(bool)> finished);
void resolveDone(
const MTPcontacts_ResolvedPeer &result,