Implement audio/video confcall invitations.

This commit is contained in:
John Preston 2025-04-01 16:09:23 +05:00
parent 021115b463
commit c5531b1bd8
15 changed files with 293 additions and 47 deletions

View file

@ -4642,6 +4642,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_call_status_failed" = "failed to connect";
"lng_call_status_ringing" = "ringing...";
"lng_call_status_busy" = "line busy";
"lng_call_status_group_invite" = "Telegram Group Call";
"lng_call_status_sure" = "Click on the Camera icon if you want to start a video call.";
"lng_call_fingerprint_tooltip" = "If the emoji on {user}'s screen are the same, this call is 100% secure";

View file

@ -1543,6 +1543,26 @@ confcallJoinUserpics: UserpicsRow {
invert: true;
}
confcallJoinUserpicsPadding: margins(0px, 0px, 0px, 16px);
confcallInviteVideo: IconButton {
width: 36px;
height: 52px;
icon: icon {{ "info/info_media_video", groupCallMemberInactiveIcon }};
iconOver: icon {{ "info/info_media_video", groupCallMemberInactiveIcon }};
iconPosition: point(-1px, -1px);
ripple: groupCallRipple;
rippleAreaPosition: point(0px, 8px);
rippleAreaSize: 36px;
}
confcallInviteVideoActive: icon {{ "info/info_media_video", groupCallActiveFg }};
confcallInviteVideoMargins: margins(0px, 0px, 10px, 0px);
confcallInviteAudio: IconButton(confcallInviteVideo) {
icon: icon {{ "menu/phone", groupCallMemberInactiveIcon }};
iconOver: icon {{ "menu/phone", groupCallMemberInactiveIcon }};
}
confcallInviteAudioActive: icon {{ "menu/phone", groupCallActiveFg }};
confcallInviteAudioMargins: margins(0px, 0px, 4px, 0px);
groupCallLinkBox: Box(confcallLinkBox) {
bg: groupCallMembersBg;

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/platform/base_platform_info.h"
#include "base/random.h"
#include "boxes/abstract_box.h"
#include "calls/group/calls_group_common.h"
#include "calls/calls_instance.h"
#include "calls/calls_panel.h"
#include "core/application.h"
@ -251,7 +252,8 @@ Call::Call(
not_null<Delegate*> delegate,
not_null<UserData*> user,
CallId conferenceId,
MsgId conferenceInviteMsgId)
MsgId conferenceInviteMsgId,
bool video)
: _delegate(delegate)
, _user(user)
, _api(&_user->session().mtp())
@ -279,10 +281,10 @@ Call::Call(
, _conferenceInviteMsgId(conferenceInviteMsgId)
, _videoIncoming(
std::make_unique<Webrtc::VideoTrack>(
StartVideoState(false)))
StartVideoState(video)))
, _videoOutgoing(
std::make_unique<Webrtc::VideoTrack>(
StartVideoState(false))) {
StartVideoState(video))) {
startWaitingTrack();
setupOutgoingVideo();
}

View file

@ -106,7 +106,8 @@ public:
not_null<Delegate*> delegate,
not_null<UserData*> user,
CallId conferenceId,
MsgId conferenceInviteMsgId);
MsgId conferenceInviteMsgId,
bool video);
[[nodiscard]] Type type() const {
return _type;

View file

@ -1010,6 +1010,7 @@ void Instance::showConferenceInvite(
const auto media = item ? item->media() : nullptr;
const auto call = media ? media->call() : nullptr;
const auto conferenceId = call ? call->conferenceId : 0;
const auto video = call->video;
if (!conferenceId
|| call->state != Data::CallState::Invitation
|| user->isSelf()) {
@ -1036,7 +1037,8 @@ void Instance::showConferenceInvite(
delegate,
user,
conferenceId,
conferenceInviteMsgId);
conferenceInviteMsgId,
video);
const auto raw = call.get();
user->session().account().sessionChanges(

View file

@ -56,6 +56,7 @@ enum class CallType;
class GroupCall;
class Panel;
struct DhConfig;
struct InviteRequest;
struct StartGroupCallArgs {
enum class JoinConfirm {
@ -73,7 +74,7 @@ struct StartConferenceCallArgs {
std::shared_ptr<TdE2E::Call> e2e;
QString linkSlug;
MsgId joinMessageId;
std::vector<not_null<UserData*>> invite;
std::vector<InviteRequest> invite;
bool migrating = false;
};

View file

@ -467,7 +467,7 @@ void Panel::initControls() {
*creating = false;
}
};
const auto create = [=](std::vector<not_null<UserData*>> users) {
const auto create = [=](std::vector<InviteRequest> users) {
if (*creating) {
return;
}
@ -481,7 +481,7 @@ void Panel::initControls() {
});
};
const auto invite = crl::guard(call, [=](
std::vector<not_null<UserData*>> users) {
std::vector<InviteRequest> users) {
create(std::move(users));
});
const auto share = crl::guard(call, [=] {
@ -1285,7 +1285,9 @@ void Panel::paint(QRect clip) {
bool Panel::handleClose() const {
if (_call) {
if (_call->state() == Call::State::WaitingUserConfirmation
|| _call->state() == Call::State::Busy) {
|| _call->state() == Call::State::Busy
|| _call->state() == Call::State::Starting
|| _call->state() == Call::State::WaitingIncoming) {
_call->hangup();
} else {
window()->hide();
@ -1424,7 +1426,10 @@ void Panel::updateStatusText(State state) {
case State::ExchangingKeys: return tr::lng_call_status_exchanging(tr::now);
case State::Waiting: return tr::lng_call_status_waiting(tr::now);
case State::Requesting: return tr::lng_call_status_requesting(tr::now);
case State::WaitingIncoming: return tr::lng_call_status_incoming(tr::now);
case State::WaitingIncoming:
return (_call->conferenceInvite()
? tr::lng_call_status_group_invite(tr::now)
: tr::lng_call_status_incoming(tr::now));
case State::Ringing: return tr::lng_call_status_ringing(tr::now);
case State::Busy: return tr::lng_call_status_busy(tr::now);
case State::WaitingUserConfirmation: return tr::lng_call_status_sure(tr::now);

View file

@ -3712,7 +3712,7 @@ void GroupCall::editParticipant(
}
void GroupCall::inviteUsers(
const std::vector<not_null<UserData*>> &users,
const std::vector<InviteRequest> &requests,
Fn<void(InviteResult)> done) {
const auto real = lookupReal();
if (!real) {
@ -3737,9 +3737,11 @@ void GroupCall::inviteUsers(
};
if (const auto call = _conferenceCall.get()) {
for (const auto &user : users) {
for (const auto &request : requests) {
using Flag = MTPphone_InviteConferenceCallParticipant::Flag;
const auto user = request.user;
_api.request(MTPphone_InviteConferenceCallParticipant(
MTP_flags(0),
MTP_flags(request.video ? Flag::f_video : Flag()),
inputCallSafe(),
user->inputUser
)).done([=](const MTPUpdates &result) {
@ -3785,7 +3787,8 @@ void GroupCall::inviteUsers(
slice.clear();
usersSlice.clear();
};
for (const auto &user : users) {
for (const auto &request : requests) {
const auto user = request.user;
owner->registerInvitedToCallUser(_id, _peer, user);
usersSlice.push_back(user);
slice.push_back(user->inputUser);
@ -3920,7 +3923,7 @@ void GroupCall::destroyScreencast() {
}
TextWithEntities ComposeInviteResultToast(
const GroupCall::InviteResult &result) {
const InviteResult &result) {
auto text = TextWithEntities();
const auto invited = int(result.invited.size());
const auto restricted = int(result.privacyRestricted.size());

View file

@ -60,6 +60,9 @@ enum class VideoQuality;
enum class Error;
} // namespace Group
struct InviteRequest;
struct InviteResult;
enum class MuteState {
Active,
PushToTalk,
@ -418,13 +421,8 @@ public:
void toggleMute(const Group::MuteRequest &data);
void changeVolume(const Group::VolumeRequest &data);
struct InviteResult {
std::vector<not_null<UserData*>> invited;
std::vector<not_null<UserData*>> alreadyIn;
std::vector<not_null<UserData*>> privacyRestricted;
};
void inviteUsers(
const std::vector<not_null<UserData*>> &users,
const std::vector<InviteRequest> &requests,
Fn<void(InviteResult)> done);
std::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();
@ -754,6 +752,6 @@ private:
};
[[nodiscard]] TextWithEntities ComposeInviteResultToast(
const GroupCall::InviteResult &result);
const InviteResult &result);
} // namespace Calls

View file

@ -41,6 +41,20 @@ namespace Window {
class SessionController;
} // namespace Window
namespace Calls {
struct InviteRequest {
not_null<UserData*> user;
bool video = false;
};
struct InviteResult {
std::vector<not_null<UserData*>> invited;
std::vector<not_null<UserData*>> alreadyIn;
std::vector<not_null<UserData*>> privacyRestricted;
};
} // namespace Calls
namespace Calls::Group {
constexpr auto kDefaultVolume = 10000;
@ -146,7 +160,7 @@ struct ConferenceCallLinkArgs {
bool joining = false;
bool migrating = false;
Fn<void(QString)> finished;
std::vector<not_null<UserData*>> invite;
std::vector<InviteRequest> invite;
ConferenceCallLinkStyleOverrides st;
};
void ShowConferenceCallLinkBox(
@ -163,7 +177,7 @@ void ExportConferenceCallLink(
struct ConferenceFactoryArgs {
std::shared_ptr<Main::SessionShow> show;
Fn<void(QString)> finished;
std::vector<not_null<UserData*>> invite;
std::vector<InviteRequest> invite;
bool joining = false;
bool migrating = false;
};

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_participants.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_group_menu.h"
#include "calls/calls_call.h"
#include "boxes/peer_lists_box.h"
@ -19,10 +20,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/profile/info_profile_icon.h"
#include "main/main_session.h"
#include "main/session/session_show.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/painter.h"
#include "apiwrap.h"
#include "lang/lang_keys.h"
#include "styles/style_boxes.h" // membersMarginTop
@ -61,6 +64,36 @@ namespace {
return result;
}
class ConfInviteRow final : public PeerListRow {
public:
using PeerListRow::PeerListRow;
void setAlreadyIn(bool alreadyIn);
void setVideo(bool video);
int elementsCount() const override;
QRect elementGeometry(int element, int outerWidth) const override;
bool elementDisabled(int element) const override;
bool elementOnlySelect(int element) const override;
void elementAddRipple(
int element,
QPoint point,
Fn<void()> updateCallback) override;
void elementsStopLastRipple() override;
void elementsPaint(
Painter &p,
int outerWidth,
bool selected,
int selectedElement) override;
private:
std::unique_ptr<Ui::RippleAnimation> _videoRipple;
std::unique_ptr<Ui::RippleAnimation> _audioRipple;
bool _alreadyIn = false;
bool _video = false;
};
class ConfInviteController final : public ContactsBoxController {
public:
ConfInviteController(
@ -69,6 +102,8 @@ public:
Fn<void()> shareLink);
[[nodiscard]] rpl::producer<bool> hasSelectedValue() const;
[[nodiscard]] std::vector<InviteRequest> requests(
const std::vector<not_null<PeerData*>> &peers) const;
protected:
void prepareViewHook() override;
@ -77,16 +112,132 @@ protected:
not_null<UserData*> user) override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowElementClicked(not_null<PeerListRow*> row, int element) override;
private:
[[nodiscard]] int fullCount() const;
void toggleRowSelected(not_null<PeerListRow*> row, bool video);
const base::flat_set<not_null<UserData*>> _alreadyIn;
const Fn<void()> _shareLink;
rpl::variable<bool> _hasSelected;
base::flat_set<not_null<UserData*>> _withVideo;
bool _lastSelectWithVideo = false;
};
void ConfInviteRow::setAlreadyIn(bool alreadyIn) {
_alreadyIn = alreadyIn;
setDisabledState(alreadyIn ? State::DisabledChecked : State::Active);
}
void ConfInviteRow::setVideo(bool video) {
_video = video;
}
int ConfInviteRow::elementsCount() const {
return _alreadyIn ? 0 : 2;
}
QRect ConfInviteRow::elementGeometry(int element, int outerWidth) const {
if (_alreadyIn || (element != 1 && element != 2)) {
return QRect();
}
const auto &st = (element == 1)
? st::confcallInviteVideo
: st::confcallInviteAudio;
const auto size = QSize(st.width, st.height);
const auto margins = (element == 1)
? st::confcallInviteVideoMargins
: st::confcallInviteAudioMargins;
const auto right = margins.right();
const auto top = margins.top();
const auto side = (element == 1)
? outerWidth
: elementGeometry(1, outerWidth).x();
const auto left = side - right - size.width();
return QRect(QPoint(left, top), size);
}
bool ConfInviteRow::elementDisabled(int element) const {
return _alreadyIn
|| (checked()
&& ((_video && element == 1) || (!_video && element == 2)));
}
bool ConfInviteRow::elementOnlySelect(int element) const {
return false;
}
void ConfInviteRow::elementAddRipple(
int element,
QPoint point,
Fn<void()> updateCallback) {
if (_alreadyIn || (element != 1 && element != 2)) {
return;
}
auto &ripple = (element == 1) ? _videoRipple : _audioRipple;
const auto &st = (element == 1)
? st::confcallInviteVideo
: st::confcallInviteAudio;
if (!ripple) {
auto mask = Ui::RippleAnimation::EllipseMask(QSize(
st.rippleAreaSize,
st.rippleAreaSize));
ripple = std::make_unique<Ui::RippleAnimation>(
st.ripple,
std::move(mask),
std::move(updateCallback));
}
ripple->add(point - st.rippleAreaPosition);
}
void ConfInviteRow::elementsStopLastRipple() {
if (_videoRipple) {
_videoRipple->lastStop();
}
if (_audioRipple) {
_audioRipple->lastStop();
}
}
void ConfInviteRow::elementsPaint(
Painter &p,
int outerWidth,
bool selected,
int selectedElement) {
if (_alreadyIn) {
return;
}
const auto paintElement = [&](int element) {
const auto &st = (element == 1)
? st::confcallInviteVideo
: st::confcallInviteAudio;
auto &ripple = (element == 1) ? _videoRipple : _audioRipple;
const auto active = checked() && ((element == 1) ? _video : !_video);
const auto geometry = elementGeometry(element, outerWidth);
if (ripple) {
ripple->paint(
p,
geometry.x() + st.rippleAreaPosition.x(),
geometry.y() + st.rippleAreaPosition.y(),
outerWidth);
if (ripple->empty()) {
ripple.reset();
}
}
const auto selected = (element == selectedElement);
const auto &icon = active
? (element == 1
? st::confcallInviteVideoActive
: st::confcallInviteAudioActive)
: (selected ? st.iconOver : st.icon);
icon.paintInCenter(p, geometry);
};
paintElement(1);
paintElement(2);
}
ConfInviteController::ConfInviteController(
not_null<Main::Session*> session,
base::flat_set<not_null<UserData*>> alreadyIn,
@ -100,6 +251,18 @@ rpl::producer<bool> ConfInviteController::hasSelectedValue() const {
return _hasSelected.value();
}
std::vector<InviteRequest> ConfInviteController::requests(
const std::vector<not_null<PeerData*>> &peers) const {
auto result = std::vector<InviteRequest>();
result.reserve(peers.size());
for (const auto &peer : peers) {
if (const auto user = peer->asUser()) {
result.push_back({ user, _withVideo.contains(user) });
}
}
return result;
}
std::unique_ptr<PeerListRow> ConfInviteController::createRow(
not_null<UserData*> user) {
if (user->isSelf()
@ -108,9 +271,12 @@ std::unique_ptr<PeerListRow> ConfInviteController::createRow(
|| user->isInaccessible()) {
return nullptr;
}
auto result = ContactsBoxController::createRow(user);
auto result = std::make_unique<ConfInviteRow>(user);
if (_alreadyIn.contains(user)) {
result->setDisabledState(PeerListRow::State::DisabledChecked);
result->setAlreadyIn(true);
}
if (_withVideo.contains(user)) {
result->setVideo(true);
}
return result;
}
@ -120,10 +286,42 @@ int ConfInviteController::fullCount() const {
}
void ConfInviteController::rowClicked(not_null<PeerListRow*> row) {
toggleRowSelected(row, _lastSelectWithVideo);
}
void ConfInviteController::rowElementClicked(
not_null<PeerListRow*> row,
int element) {
if (row->checked()) {
static_cast<ConfInviteRow*>(row.get())->setVideo(element == 1);
_lastSelectWithVideo = (element == 1);
} else if (element == 1) {
toggleRowSelected(row, true);
} else if (element == 2) {
toggleRowSelected(row, false);
}
}
void ConfInviteController::toggleRowSelected(
not_null<PeerListRow*> row,
bool video) {
auto count = fullCount();
auto limit = Data::kMaxConferenceMembers;
if (count < limit || row->checked()) {
const auto real = static_cast<ConfInviteRow*>(row.get());
if (!row->checked()) {
real->setVideo(video);
_lastSelectWithVideo = video;
}
const auto user = row->peer()->asUser();
if (!row->checked() && video) {
_withVideo.emplace(user);
} else {
_withVideo.remove(user);
}
delegate()->peerListSetRowChecked(row, !row->checked());
// row may have been destroyed here, from search.
_hasSelected = (delegate()->peerListSelectedRowsCount() > 0);
} else {
delegate()->peerListUiShow()->showToast(
@ -311,13 +509,7 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
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;
const auto done = [=](GroupCall::InviteResult result) {
const auto done = [=](InviteResult result) {
(*close)();
if (result.invited.empty()
&& result.privacyRestricted.empty()) {
@ -325,7 +517,9 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
}
showToast({ ComposeInviteResultToast(result) });
};
call->inviteUsers(users, done);
call->inviteUsers(
raw->requests(box->collectSelectedRows()),
done);
});
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
@ -354,7 +548,12 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
if (!call) {
return;
}
call->inviteUsers(users, [=](GroupCall::InviteResult result) {
auto requests = ranges::views::all(
users
) | ranges::views::transform([](not_null<UserData*> user) {
return InviteRequest{ user };
}) | ranges::to_vector;
call->inviteUsers(std::move(requests), [=](InviteResult result) {
if (result.invited.size() == 1) {
showToast(tr::lng_group_call_invite_done_user(
tr::now,
@ -463,7 +662,7 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<Call*> call,
Fn<void(std::vector<not_null<UserData*>>)> inviteUsers,
Fn<void(std::vector<InviteRequest>)> inviteUsers,
Fn<void()> shareLink) {
const auto user = call->user();
const auto weak = base::make_weak(call);
@ -486,13 +685,7 @@ object_ptr<Ui::BoxContent> PrepareInviteBox(
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));
inviteUsers(raw->requests(box->collectSelectedRows()));
});
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Calls {
class Call;
class GroupCall;
struct InviteRequest;
} // namespace Calls
namespace Calls::Group {
@ -83,7 +84,7 @@ private:
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<Call*> call,
Fn<void(std::vector<not_null<UserData*>>)> inviteUsers,
Fn<void(std::vector<InviteRequest>)> inviteUsers,
Fn<void()> shareLink);
} // namespace Calls::Group

View file

@ -1006,8 +1006,8 @@ void Panel::migrationShowShareLink() {
{ .st = DarkConferenceCallLinkStyle() });
}
void Panel::migrationInviteUsers(std::vector<not_null<UserData*>> users) {
const auto done = [=](GroupCall::InviteResult result) {
void Panel::migrationInviteUsers(std::vector<InviteRequest> users) {
const auto done = [=](InviteResult result) {
showToast({ ComposeInviteResultToast(result) });
};
_call->inviteUsers(std::move(users), crl::guard(this, done));

View file

@ -73,6 +73,10 @@ struct CallSignalBars;
struct CallBodyLayout;
} // namespace style
namespace Calls {
struct InviteRequest;
} // namespace Calls
namespace Calls::Group {
class Toasts;
@ -116,7 +120,7 @@ public:
[[nodiscard]] bool isLayerShown() const;
void migrationShowShareLink();
void migrationInviteUsers(std::vector<not_null<UserData*>> users);
void migrationInviteUsers(std::vector<InviteRequest> users);
void minimize();
void toggleFullScreen();

View file

@ -491,6 +491,7 @@ Call ComputeCallData(const MTPDmessageActionConferenceCall &call) {
: call.is_active()
? CallState::Active
: CallState::Invitation),
.video = call.is_video(),
};
}