mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-05-08 11:09:45 +00:00
1214 lines
34 KiB
C++
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
|