tdesktop/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp
2025-05-01 11:13:20 +04:00

1214 lines
34 KiB
C++

/*
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 "calls/group/calls_group_invite_controller.h"
#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 "calls/calls_instance.h"
#include "core/application.h"
#include "boxes/peer_lists_box.h"
#include "data/data_user.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_group_call.h"
#include "info/profile/info_profile_icon.h"
#include "main/session/session_show.h"
#include "main/main_app_config.h"
#include "main/main_session.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/widgets/scroll_area.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "apiwrap.h"
#include "lang/lang_keys.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h" // membersMarginTop
#include "styles/style_calls.h"
#include "styles/style_dialogs.h" // searchedBarHeight
#include "styles/style_layers.h" // boxWideWidth
namespace Calls::Group {
namespace {
[[nodiscard]] object_ptr<Ui::RpWidget> CreateSectionSubtitle(
QWidget *parent,
rpl::producer<QString> text) {
auto result = object_ptr<Ui::FixedHeightWidget>(
parent,
st::searchedBarHeight);
const auto raw = result.data();
raw->paintRequest(
) | rpl::start_with_next([=](QRect clip) {
auto p = QPainter(raw);
p.fillRect(clip, st::groupCallMembersBgOver);
}, raw->lifetime());
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
std::move(text),
st::groupCallBoxLabel);
raw->widthValue(
) | rpl::start_with_next([=](int width) {
const auto padding = st::groupCallInviteDividerPadding;
const auto available = width - padding.left() - padding.right();
label->resizeToNaturalWidth(available);
label->moveToLeft(padding.left(), padding.top(), width);
}, label->lifetime());
return result;
}
struct ConfInviteStyles {
const style::IconButton *video = nullptr;
const style::icon *videoActive = nullptr;
const style::IconButton *audio = nullptr;
const style::icon *audioActive = nullptr;
const style::SettingsButton *inviteViaLink = nullptr;
const style::icon *inviteViaLinkIcon = nullptr;
};
class ConfInviteRow final : public PeerListRow {
public:
ConfInviteRow(not_null<UserData*> user, const ConfInviteStyles &st);
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:
[[nodiscard]] const style::IconButton &buttonSt(int element) const;
const ConfInviteStyles &_st;
std::unique_ptr<Ui::RippleAnimation> _videoRipple;
std::unique_ptr<Ui::RippleAnimation> _audioRipple;
bool _alreadyIn = false;
bool _video = false;
};
struct PrioritizedSelector {
object_ptr<Ui::RpWidget> content = { nullptr };
Fn<void()> init;
Fn<bool(int, int, int)> overrideKey;
Fn<void(PeerListRowId)> deselect;
Fn<void()> activate;
rpl::producer<Ui::ScrollToRequest> scrollToRequests;
};
class ConfInviteController final : public ContactsBoxController {
public:
ConfInviteController(
not_null<Main::Session*> session,
ConfInviteStyles st,
base::flat_set<not_null<UserData*>> alreadyIn,
Fn<void()> shareLink,
std::vector<not_null<UserData*>> prioritize);
[[nodiscard]] rpl::producer<bool> hasSelectedValue() const;
[[nodiscard]] std::vector<InviteRequest> requests(
const std::vector<not_null<PeerData*>> &peers) const;
void noSearchSubmit();
[[nodiscard]] auto prioritizeScrollRequests() const
-> rpl::producer<Ui::ScrollToRequest>;
protected:
void prepareViewHook() override;
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowElementClicked(not_null<PeerListRow*> row, int element) override;
bool handleDeselectForeignRow(PeerListRowId itemId) override;
bool overrideKeyboardNavigation(
int direction,
int fromIndex,
int toIndex) override;
private:
[[nodiscard]] int fullCount() const;
void toggleRowSelected(not_null<PeerListRow*> row, bool video);
[[nodiscard]] bool toggleRowGetChecked(
not_null<PeerListRow*> row,
bool video);
void addShareLinkButton();
void addPriorityInvites();
const ConfInviteStyles _st;
const base::flat_set<not_null<UserData*>> _alreadyIn;
const std::vector<not_null<UserData*>> _prioritize;
const Fn<void()> _shareLink;
PrioritizedSelector _prioritizeRows;
rpl::event_stream<Ui::ScrollToRequest> _prioritizeScrollRequests;
base::flat_set<not_null<UserData*>> _skip;
rpl::variable<bool> _hasSelected;
base::flat_set<not_null<UserData*>> _withVideo;
bool _lastSelectWithVideo = false;
};
[[nodiscard]] ConfInviteStyles ConfInviteDarkStyles() {
return {
.video = &st::confcallInviteVideo,
.videoActive = &st::confcallInviteVideoActive,
.audio = &st::confcallInviteAudio,
.audioActive = &st::confcallInviteAudioActive,
.inviteViaLink = &st::groupCallInviteLink,
.inviteViaLinkIcon = &st::groupCallInviteLinkIcon,
};
}
[[nodiscard]] ConfInviteStyles ConfInviteDefaultStyles() {
return {
.video = &st::createCallVideo,
.videoActive = &st::createCallVideoActive,
.audio = &st::createCallAudio,
.audioActive = &st::createCallAudioActive,
.inviteViaLink = &st::createCallInviteLink,
.inviteViaLinkIcon = &st::createCallInviteLinkIcon,
};
}
ConfInviteRow::ConfInviteRow(not_null<UserData*> user, const ConfInviteStyles &st)
: PeerListRow(user)
, _st(st) {
}
void ConfInviteRow::setAlreadyIn(bool alreadyIn) {
_alreadyIn = alreadyIn;
setDisabledState(alreadyIn ? State::DisabledChecked : State::Active);
}
void ConfInviteRow::setVideo(bool video) {
_video = video;
}
const style::IconButton &ConfInviteRow::buttonSt(int element) const {
return (element == 1)
? (_st.video ? *_st.video : st::createCallVideo)
: (_st.audio ? *_st.audio : st::createCallAudio);
}
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 = buttonSt(element);
const auto size = QSize(st.width, st.height);
const auto margins = (element == 1)
? st::createCallVideoMargins
: st::createCallAudioMargins;
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 = buttonSt(element);
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 = buttonSt(element);
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.videoActive
? *_st.videoActive
: st::createCallVideoActive)
: (_st.audioActive
? *_st.audioActive
: st::createCallAudioActive))
: (selected ? st.iconOver : st.icon);
icon.paintInCenter(p, geometry);
};
paintElement(1);
paintElement(2);
}
[[nodiscard]] PrioritizedSelector PrioritizedInviteSelector(
const ConfInviteStyles &st,
std::vector<not_null<UserData*>> users,
Fn<bool(not_null<PeerListRow*>, bool, anim::type)> toggleGetChecked,
Fn<bool()> lastSelectWithVideo,
Fn<void(bool)> setLastSelectWithVideo) {
class PrioritizedController final : public PeerListController {
public:
PrioritizedController(
const ConfInviteStyles &st,
std::vector<not_null<UserData*>> users,
Fn<bool(
not_null<PeerListRow*>,
bool,
anim::type)> toggleGetChecked,
Fn<bool()> lastSelectWithVideo,
Fn<void(bool)> setLastSelectWithVideo)
: _st(st)
, _users(std::move(users))
, _toggleGetChecked(std::move(toggleGetChecked))
, _lastSelectWithVideo(std::move(lastSelectWithVideo))
, _setLastSelectWithVideo(std::move(setLastSelectWithVideo)) {
Expects(!_users.empty());
}
void prepare() override {
for (const auto user : _users) {
delegate()->peerListAppendRow(
std::make_unique<ConfInviteRow>(user, _st));
}
delegate()->peerListRefreshRows();
}
void loadMoreRows() override {
}
void rowClicked(not_null<PeerListRow*> row) override {
toggleRowSelected(row, _lastSelectWithVideo());
}
void rowElementClicked(
not_null<PeerListRow*> row,
int element) override {
if (row->checked()) {
static_cast<ConfInviteRow*>(row.get())->setVideo(
element == 1);
_setLastSelectWithVideo(element == 1);
} else if (element == 1) {
toggleRowSelected(row, true);
} else if (element == 2) {
toggleRowSelected(row, false);
}
}
void toggleRowSelected(not_null<PeerListRow*> row, bool video) {
delegate()->peerListSetRowChecked(
row,
_toggleGetChecked(row, video, anim::type::normal));
}
Main::Session &session() const override {
return _users.front()->session();
}
void toggleFirst() {
rowClicked(delegate()->peerListRowAt(0));
}
private:
const ConfInviteStyles &_st;
std::vector<not_null<UserData*>> _users;
Fn<bool(not_null<PeerListRow*>, bool, anim::type)> _toggleGetChecked;
Fn<bool()> _lastSelectWithVideo;
Fn<void(bool)> _setLastSelectWithVideo;
};
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = result.data();
const auto delegate = container->lifetime().make_state<
PeerListContentDelegateSimple
>();
const auto controller = container->lifetime(
).make_state<PrioritizedController>(
st,
users,
toggleGetChecked,
lastSelectWithVideo,
setLastSelectWithVideo);
controller->setStyleOverrides(&st::createCallList);
const auto content = container->add(object_ptr<PeerListContent>(
container,
controller));
const auto activate = [=] {
content->submitted();
};
content->noSearchSubmits() | rpl::start_with_next([=] {
controller->toggleFirst();
}, content->lifetime());
delegate->setContent(content);
controller->setDelegate(delegate);
Ui::AddDivider(container);
const auto overrideKey = [=](int direction, int from, int to) {
if (!content->isVisible()) {
return false;
} else if (direction > 0 && from < 0 && to >= 0) {
if (content->hasSelection()) {
const auto was = content->selectedIndex();
const auto now = content->selectSkip(1).reallyMovedTo;
if (was != now) {
return true;
}
content->clearSelection();
} else {
content->selectSkip(1);
return true;
}
} else if (direction < 0 && to < 0) {
if (!content->hasSelection()) {
content->selectLast();
} else if (from >= 0 || content->hasSelection()) {
content->selectSkip(-1);
}
}
return false;
};
const auto deselect = [=](PeerListRowId rowId) {
if (const auto row = delegate->peerListFindRow(rowId)) {
delegate->peerListSetRowChecked(row, false);
}
};
const auto init = [=] {
for (const auto &user : users) {
if (const auto row = delegate->peerListFindRow(user->id.value)) {
delegate->peerListSetRowChecked(
row,
toggleGetChecked(row, false, anim::type::instant));
}
}
};
return {
.content = std::move(result),
.init = init,
.overrideKey = overrideKey,
.deselect = deselect,
.activate = activate,
.scrollToRequests = content->scrollToRequests(),
};
}
ConfInviteController::ConfInviteController(
not_null<Main::Session*> session,
ConfInviteStyles st,
base::flat_set<not_null<UserData*>> alreadyIn,
Fn<void()> shareLink,
std::vector<not_null<UserData*>> prioritize)
: ContactsBoxController(session)
, _st(st)
, _alreadyIn(std::move(alreadyIn))
, _prioritize(std::move(prioritize))
, _shareLink(std::move(shareLink)) {
if (!_shareLink) {
_skip.reserve(_prioritize.size());
for (const auto user : _prioritize) {
_skip.emplace(user);
}
}
}
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()
|| user->isBot()
|| user->isServiceUser()
|| user->isInaccessible()
|| _skip.contains(user)) {
return nullptr;
}
auto result = std::make_unique<ConfInviteRow>(user, _st);
if (_alreadyIn.contains(user)) {
result->setAlreadyIn(true);
}
if (_withVideo.contains(user)) {
result->setVideo(true);
}
return result;
}
int ConfInviteController::fullCount() const {
return _alreadyIn.size()
+ delegate()->peerListSelectedRowsCount()
+ (_alreadyIn.contains(session().user()) ? 1 : 0);
}
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);
}
}
bool ConfInviteController::handleDeselectForeignRow(PeerListRowId itemId) {
if (_prioritizeRows.deselect) {
const auto userId = peerToUser(PeerId(itemId));
if (ranges::contains(_prioritize, session().data().user(userId))) {
_prioritizeRows.deselect(itemId);
return true;
}
}
return false;
}
bool ConfInviteController::overrideKeyboardNavigation(
int direction,
int fromIndex,
int toIndex) {
return _prioritizeRows.overrideKey
&& _prioritizeRows.overrideKey(direction, fromIndex, toIndex);
}
void ConfInviteController::toggleRowSelected(
not_null<PeerListRow*> row,
bool video) {
delegate()->peerListSetRowChecked(row, toggleRowGetChecked(row, video));
// row may have been destroyed here, from search.
_hasSelected = (delegate()->peerListSelectedRowsCount() > 0);
}
bool ConfInviteController::toggleRowGetChecked(
not_null<PeerListRow*> row,
bool video) {
auto count = fullCount();
const auto conferenceLimit = session().appConfig().confcallSizeLimit();
if (!row->checked() && count >= conferenceLimit) {
delegate()->peerListUiShow()->showToast(
tr::lng_group_call_invite_limit(tr::now));
return false;
}
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);
}
return !row->checked();
}
void ConfInviteController::noSearchSubmit() {
if (const auto onstack = _prioritizeRows.activate) {
onstack();
} else if (delegate()->peerListFullRowsCount() > 0) {
rowClicked(delegate()->peerListRowAt(0));
}
}
auto ConfInviteController::prioritizeScrollRequests() const
-> rpl::producer<Ui::ScrollToRequest> {
return _prioritizeScrollRequests.events();
}
void ConfInviteController::prepareViewHook() {
if (_shareLink) {
addShareLinkButton();
} else if (!_prioritize.empty()) {
addPriorityInvites();
}
}
void ConfInviteController::addPriorityInvites() {
const auto toggleGetChecked = [=](
not_null<PeerListRow*> row,
bool video,
anim::type animated) {
const auto result = toggleRowGetChecked(row, video);
delegate()->peerListSetForeignRowChecked(
row,
result,
animated);
_hasSelected = (delegate()->peerListSelectedRowsCount() > 0);
return result;
};
_prioritizeRows = PrioritizedInviteSelector(
_st,
_prioritize,
toggleGetChecked,
[=] { return _lastSelectWithVideo; },
[=](bool video) { _lastSelectWithVideo = video; });
if (auto &scrollTo = _prioritizeRows.scrollToRequests) {
std::move(
scrollTo
) | rpl::start_to_stream(_prioritizeScrollRequests, lifetime());
}
if (const auto onstack = _prioritizeRows.init) {
onstack();
// Force finishing in instant adding checked rows bunch.
delegate()->peerListAddSelectedPeers(
std::vector<not_null<PeerData*>>());
}
delegate()->peerListSetAboveWidget(std::move(_prioritizeRows.content));
}
void ConfInviteController::addShareLinkButton() {
auto button = object_ptr<Ui::PaddingWrap<Ui::SettingsButton>>(
nullptr,
object_ptr<Ui::SettingsButton>(
nullptr,
tr::lng_profile_add_via_link(),
(_st.inviteViaLink
? *_st.inviteViaLink
: st::createCallInviteLink)),
style::margins(0, st::membersMarginTop, 0, 0));
const auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(
button->entity(),
(_st.inviteViaLinkIcon
? *_st.inviteViaLinkIcon
: st::createCallInviteLinkIcon),
QPoint());
button->entity()->heightValue(
) | rpl::start_with_next([=](int height) {
icon->moveToLeft(
st::createCallInviteLinkIconPosition.x(),
(height - st::groupCallInviteLinkIcon.height()) / 2);
}, icon->lifetime());
button->entity()->setClickedCallback(_shareLink);
button->entity()->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::Enter);
}) | rpl::start_with_next([=] {
delegate()->peerListMouseLeftGeometry();
}, button->lifetime());
delegate()->peerListSetAboveWidget(std::move(button));
}
} // namespace
InviteController::InviteController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn)
: ParticipantsBoxController(CreateTag{}, nullptr, peer, Role::Members)
, _peer(peer)
, _alreadyIn(std::move(alreadyIn)) {
SubscribeToMigration(
_peer,
lifetime(),
[=](not_null<ChannelData*> channel) { _peer = channel; });
}
void InviteController::prepare() {
delegate()->peerListSetHideEmpty(true);
ParticipantsBoxController::prepare();
delegate()->peerListSetAboveWidget(CreateSectionSubtitle(
nullptr,
tr::lng_group_call_invite_members()));
delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle(
nullptr,
tr::lng_group_call_invite_members()));
}
void InviteController::rowClicked(not_null<PeerListRow*> row) {
delegate()->peerListSetRowChecked(row, !row->checked());
}
base::unique_qptr<Ui::PopupMenu> InviteController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
return nullptr;
}
void InviteController::itemDeselectedHook(not_null<PeerData*> peer) {
}
bool InviteController::hasRowFor(not_null<PeerData*> peer) const {
return (delegate()->peerListFindRow(peer->id.value) != nullptr);
}
bool InviteController::isAlreadyIn(not_null<UserData*> user) const {
return _alreadyIn.contains(user);
}
std::unique_ptr<PeerListRow> InviteController::createRow(
not_null<PeerData*> participant) const {
const auto user = participant->asUser();
if (!user
|| user->isSelf()
|| user->isBot()
|| user->isInaccessible()) {
return nullptr;
}
auto result = std::make_unique<PeerListRow>(user);
_rowAdded.fire_copy(user);
_inGroup.emplace(user);
if (isAlreadyIn(user)) {
result->setDisabledState(PeerListRow::State::DisabledChecked);
}
return result;
}
auto InviteController::peersWithRows() const
-> not_null<const base::flat_set<not_null<UserData*>>*> {
return &_inGroup;
}
rpl::producer<not_null<UserData*>> InviteController::rowAdded() const {
return _rowAdded.events();
}
InviteContactsController::InviteContactsController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn,
not_null<const base::flat_set<not_null<UserData*>>*> inGroup,
rpl::producer<not_null<UserData*>> discoveredInGroup)
: AddParticipantsBoxController(peer, std::move(alreadyIn))
, _inGroup(inGroup)
, _discoveredInGroup(std::move(discoveredInGroup)) {
}
void InviteContactsController::prepareViewHook() {
AddParticipantsBoxController::prepareViewHook();
delegate()->peerListSetAboveWidget(CreateSectionSubtitle(
nullptr,
tr::lng_contacts_header()));
delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle(
nullptr,
tr::lng_group_call_invite_search_results()));
std::move(
_discoveredInGroup
) | rpl::start_with_next([=](not_null<UserData*> user) {
if (auto row = delegate()->peerListFindRow(user->id.value)) {
delegate()->peerListRemoveRow(row);
}
}, _lifetime);
}
std::unique_ptr<PeerListRow> InviteContactsController::createRow(
not_null<UserData*> user) {
return _inGroup->contains(user)
? nullptr
: AddParticipantsBoxController::createRow(user);
}
object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<GroupCall*> call,
Fn<void(TextWithEntities&&)> showToast,
Fn<void()> shareConferenceLink) {
const auto real = call->lookupReal();
if (!real) {
return nullptr;
}
const auto peer = call->peer();
const auto conference = call->conference();
const auto weak = base::make_weak(call);
const auto &invited = peer->owner().invitedToCallUsers(real->id());
auto alreadyIn = base::flat_set<not_null<UserData*>>();
alreadyIn.reserve(invited.size() + real->participants().size() + 1);
alreadyIn.emplace(peer->session().user());
for (const auto &participant : real->participants()) {
if (const auto user = participant.peer->asUser()) {
alreadyIn.emplace(user);
}
}
for (const auto &[user, calling] : invited) {
if (!conference || calling) {
alreadyIn.emplace(user);
}
}
if (conference) {
const auto close = std::make_shared<Fn<void()>>();
const auto shareLink = [=] {
Expects(shareConferenceLink != nullptr);
shareConferenceLink();
(*close)();
};
auto controller = std::make_unique<ConfInviteController>(
&real->session(),
ConfInviteDarkStyles(),
alreadyIn,
shareLink,
std::vector<not_null<UserData*>>());
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_confcall_add(), [=] {
const auto call = weak.get();
if (!call) {
return;
}
const auto done = [=](InviteResult result) {
(*close)();
showToast({ ComposeInviteResultToast(result) });
};
call->inviteUsers(
raw->requests(box->collectSelectedRows()),
done);
});
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}, box->lifetime());
*close = crl::guard(box, [=] { box->closeBox(); });
};
return Box<PeerListBox>(std::move(controller), initBox);
}
auto controller = std::make_unique<InviteController>(peer, alreadyIn);
controller->setStyleOverrides(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
auto contactsController = std::make_unique<InviteContactsController>(
peer,
std::move(alreadyIn),
controller->peersWithRows(),
controller->rowAdded());
contactsController->setStyleOverrides(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
const auto invite = [=](const std::vector<not_null<UserData*>> &users) {
const auto call = weak.get();
if (!call) {
return;
}
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,
lt_user,
Ui::Text::Bold(result.invited.front()->firstName),
Ui::Text::WithEntities));
} else if (result.invited.size() > 1) {
showToast(tr::lng_group_call_invite_done_many(
tr::now,
lt_count,
result.invited.size(),
Ui::Text::RichLangValue));
}
});
};
const auto inviteWithAdd = [=](
std::shared_ptr<Ui::Show> show,
const std::vector<not_null<UserData*>> &users,
const std::vector<not_null<UserData*>> &nonMembers,
Fn<void()> finish) {
peer->session().api().chatParticipants().add(
show,
peer,
nonMembers,
true,
[=](bool) { invite(users); finish(); });
};
const auto inviteWithConfirmation = [=](
not_null<PeerListsBox*> parentBox,
const std::vector<not_null<UserData*>> &users,
const std::vector<not_null<UserData*>> &nonMembers,
Fn<void()> finish) {
if (nonMembers.empty()) {
invite(users);
finish();
return;
}
const auto name = peer->name();
const auto text = (nonMembers.size() == 1)
? tr::lng_group_call_add_to_group_one(
tr::now,
lt_user,
nonMembers.front()->shortName(),
lt_group,
name)
: (nonMembers.size() < users.size())
? tr::lng_group_call_add_to_group_some(tr::now, lt_group, name)
: tr::lng_group_call_add_to_group_all(tr::now, lt_group, name);
const auto shared = std::make_shared<QPointer<Ui::GenericBox>>();
const auto finishWithConfirm = [=] {
if (*shared) {
(*shared)->closeBox();
}
finish();
};
const auto done = [=] {
const auto show = (*shared) ? (*shared)->uiShow() : nullptr;
inviteWithAdd(show, users, nonMembers, finishWithConfirm);
};
auto box = ConfirmBox({
.text = text,
.confirmed = done,
.confirmText = tr::lng_participant_invite(),
});
*shared = box.data();
parentBox->getDelegate()->showBox(
std::move(box),
Ui::LayerOption::KeepOther,
anim::type::normal);
};
auto initBox = [=, controller = controller.get()](
not_null<PeerListsBox*> box) {
box->setTitle(tr::lng_group_call_invite_title());
box->addButton(tr::lng_group_call_invite_button(), [=] {
const auto rows = box->collectSelectedRows();
const auto users = ranges::views::all(
rows
) | ranges::views::transform([](not_null<PeerData*> peer) {
return not_null<UserData*>(peer->asUser());
}) | ranges::to_vector;
const auto nonMembers = ranges::views::all(
users
) | ranges::views::filter([&](not_null<UserData*> user) {
return !controller->hasRowFor(user);
}) | ranges::to_vector;
const auto finish = [box = Ui::MakeWeak(box)]() {
if (box) {
box->closeBox();
}
};
inviteWithConfirmation(box, users, nonMembers, finish);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
};
auto controllers = std::vector<std::unique_ptr<PeerListController>>();
controllers.push_back(std::move(controller));
controllers.push_back(std::move(contactsController));
return Box<PeerListsBox>(std::move(controllers), initBox);
}
object_ptr<Ui::BoxContent> PrepareInviteBox(
not_null<Call*> call,
Fn<void(std::vector<InviteRequest>)> 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(),
ConfInviteDarkStyles(),
alreadyIn,
shareLink,
std::vector<not_null<UserData*>>());
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;
}
inviteUsers(raw->requests(box->collectSelectedRows()));
});
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}, box->lifetime());
};
return Box<PeerListBox>(std::move(controller), initBox);
}
not_null<Ui::RpWidget*> CreateReActivateHeader(not_null<QWidget*> parent) {
const auto result = Ui::CreateChild<Ui::VerticalLayout>(parent);
result->add(
MakeJoinCallLogo(result),
st::boxRowPadding + st::confcallLinkHeaderIconPadding);
result->add(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
result,
object_ptr<Ui::FlatLabel>(
result,
tr::lng_confcall_inactive_title(),
st::boxTitle)),
st::boxRowPadding + st::confcallLinkTitlePadding);
result->add(
object_ptr<Ui::FlatLabel>(
result,
tr::lng_confcall_inactive_about(),
st::confcallLinkCenteredText),
st::boxRowPadding + st::confcallLinkTitlePadding
)->setTryMakeSimilarLines(true);
Ui::AddDivider(result);
return result;
}
void InitReActivate(not_null<PeerListBox*> box) {
box->setTitle(rpl::producer<TextWithEntities>(nullptr));
box->setNoContentMargin(true);
const auto header = CreateReActivateHeader(box);
header->resizeToWidth(st::boxWideWidth);
header->heightValue() | rpl::start_with_next([=](int height) {
box->setAddedTopScrollSkip(height, true);
}, header->lifetime());
header->moveToLeft(0, 0);
}
object_ptr<Ui::BoxContent> PrepareInviteToEmptyBox(
std::shared_ptr<Data::GroupCall> call,
MsgId inviteMsgId,
std::vector<not_null<UserData*>> prioritize) {
auto controller = std::make_unique<ConfInviteController>(
&call->session(),
ConfInviteDefaultStyles(),
base::flat_set<not_null<UserData*>>(),
nullptr,
std::move(prioritize));
const auto raw = controller.get();
raw->setStyleOverrides(&st::createCallList);
const auto initBox = [=](not_null<PeerListBox*> box) {
InitReActivate(box);
box->noSearchSubmits() | rpl::start_with_next([=] {
raw->noSearchSubmit();
}, box->lifetime());
raw->prioritizeScrollRequests(
) | rpl::start_with_next([=](Ui::ScrollToRequest request) {
box->scrollTo(request);
}, box->lifetime());
const auto join = [=] {
const auto weak = Ui::MakeWeak(box);
auto selected = raw->requests(box->collectSelectedRows());
Core::App().calls().startOrJoinConferenceCall({
.call = call,
.joinMessageId = inviteMsgId,
.invite = std::move(selected),
});
if (const auto strong = weak.data()) {
strong->closeBox();
}
};
box->addButton(
rpl::conditional(
raw->hasSelectedValue(),
tr::lng_group_call_confcall_add(),
tr::lng_create_group_create()),
join);
box->addButton(tr::lng_close(), [=] {
box->closeBox();
});
};
return Box<PeerListBox>(std::move(controller), initBox);
}
object_ptr<Ui::BoxContent> PrepareCreateCallBox(
not_null<::Window::SessionController*> window,
Fn<void()> created,
MsgId discardedInviteMsgId,
std::vector<not_null<UserData*>> prioritize) {
struct State {
bool creatingLink = false;
QPointer<PeerListBox> box;
};
const auto state = std::make_shared<State>();
const auto finished = [=](bool ok) {
if (!ok) {
state->creatingLink = false;
} else {
if (const auto strong = state->box.data()) {
strong->closeBox();
}
if (const auto onstack = created) {
onstack();
}
}
};
const auto shareLink = [=] {
if (state->creatingLink) {
return;
}
state->creatingLink = true;
MakeConferenceCall({
.show = window->uiShow(),
.finished = finished,
});
};
auto controller = std::make_unique<ConfInviteController>(
&window->session(),
ConfInviteDefaultStyles(),
base::flat_set<not_null<UserData*>>(),
discardedInviteMsgId ? Fn<void()>() : shareLink,
std::move(prioritize));
const auto raw = controller.get();
if (discardedInviteMsgId) {
raw->setStyleOverrides(&st::createCallList);
}
const auto initBox = [=](not_null<PeerListBox*> box) {
if (discardedInviteMsgId) {
InitReActivate(box);
} else {
box->setTitle(tr::lng_confcall_create_title());
}
box->noSearchSubmits() | rpl::start_with_next([=] {
raw->noSearchSubmit();
}, box->lifetime());
raw->prioritizeScrollRequests(
) | rpl::start_with_next([=](Ui::ScrollToRequest request) {
box->scrollTo(request);
}, box->lifetime());
const auto create = [=] {
auto selected = raw->requests(box->collectSelectedRows());
if (selected.size() != 1 || discardedInviteMsgId) {
Core::App().calls().startOrJoinConferenceCall({
.show = window->uiShow(),
.invite = std::move(selected),
});
} else {
const auto &invite = selected.front();
Core::App().calls().startOutgoingCall(
invite.user,
invite.video);
}
finished(true);
};
box->addButton(
rpl::conditional(
raw->hasSelectedValue(),
tr::lng_group_call_confcall_add(),
tr::lng_create_group_create()),
create);
box->addButton(tr::lng_close(), [=] {
box->closeBox();
});
};
auto result = Box<PeerListBox>(std::move(controller), initBox);
state->box = result.data();
return result;
}
} // namespace Calls::Group