/* 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 CreateSectionSubtitle( QWidget *parent, rpl::producer text) { auto result = object_ptr( 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( 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 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 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 _videoRipple; std::unique_ptr _audioRipple; bool _alreadyIn = false; bool _video = false; }; struct PrioritizedSelector { object_ptr content = { nullptr }; Fn init; Fn overrideKey; Fn deselect; Fn activate; rpl::producer scrollToRequests; }; class ConfInviteController final : public ContactsBoxController { public: ConfInviteController( not_null session, ConfInviteStyles st, base::flat_set> alreadyIn, Fn shareLink, std::vector> prioritize); [[nodiscard]] rpl::producer hasSelectedValue() const; [[nodiscard]] std::vector requests( const std::vector> &peers) const; void noSearchSubmit(); [[nodiscard]] auto prioritizeScrollRequests() const -> rpl::producer; protected: void prepareViewHook() override; std::unique_ptr createRow( not_null user) override; void rowClicked(not_null row) override; void rowElementClicked(not_null 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 row, bool video); [[nodiscard]] bool toggleRowGetChecked( not_null row, bool video); void addShareLinkButton(); void addPriorityInvites(); const ConfInviteStyles _st; const base::flat_set> _alreadyIn; const std::vector> _prioritize; const Fn _shareLink; PrioritizedSelector _prioritizeRows; rpl::event_stream _prioritizeScrollRequests; base::flat_set> _skip; rpl::variable _hasSelected; base::flat_set> _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 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 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( 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> users, Fn, bool, anim::type)> toggleGetChecked, Fn lastSelectWithVideo, Fn setLastSelectWithVideo) { class PrioritizedController final : public PeerListController { public: PrioritizedController( const ConfInviteStyles &st, std::vector> users, Fn, bool, anim::type)> toggleGetChecked, Fn lastSelectWithVideo, Fn 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(user, _st)); } delegate()->peerListRefreshRows(); } void loadMoreRows() override { } void rowClicked(not_null row) override { toggleRowSelected(row, _lastSelectWithVideo()); } void rowElementClicked( not_null row, int element) override { if (row->checked()) { static_cast(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 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> _users; Fn, bool, anim::type)> _toggleGetChecked; Fn _lastSelectWithVideo; Fn _setLastSelectWithVideo; }; auto result = object_ptr((QWidget*)nullptr); const auto container = result.data(); const auto delegate = container->lifetime().make_state< PeerListContentDelegateSimple >(); const auto controller = container->lifetime( ).make_state( st, users, toggleGetChecked, lastSelectWithVideo, setLastSelectWithVideo); controller->setStyleOverrides(&st::createCallList); const auto content = container->add(object_ptr( 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 session, ConfInviteStyles st, base::flat_set> alreadyIn, Fn shareLink, std::vector> 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 ConfInviteController::hasSelectedValue() const { return _hasSelected.value(); } std::vector ConfInviteController::requests( const std::vector> &peers) const { auto result = std::vector(); 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 ConfInviteController::createRow( not_null user) { if (user->isSelf() || user->isBot() || user->isServiceUser() || user->isInaccessible() || _skip.contains(user)) { return nullptr; } auto result = std::make_unique(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 row) { toggleRowSelected(row, _lastSelectWithVideo); } void ConfInviteController::rowElementClicked( not_null row, int element) { if (row->checked()) { static_cast(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 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 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(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 { return _prioritizeScrollRequests.events(); } void ConfInviteController::prepareViewHook() { if (_shareLink) { addShareLinkButton(); } else if (!_prioritize.empty()) { addPriorityInvites(); } } void ConfInviteController::addPriorityInvites() { const auto toggleGetChecked = [=]( not_null 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>()); } delegate()->peerListSetAboveWidget(std::move(_prioritizeRows.content)); } void ConfInviteController::addShareLinkButton() { auto button = object_ptr>( nullptr, object_ptr( 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( 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 e) { return (e->type() == QEvent::Enter); }) | rpl::start_with_next([=] { delegate()->peerListMouseLeftGeometry(); }, button->lifetime()); delegate()->peerListSetAboveWidget(std::move(button)); } } // namespace InviteController::InviteController( not_null peer, base::flat_set> alreadyIn) : ParticipantsBoxController(CreateTag{}, nullptr, peer, Role::Members) , _peer(peer) , _alreadyIn(std::move(alreadyIn)) { SubscribeToMigration( _peer, lifetime(), [=](not_null 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 row) { delegate()->peerListSetRowChecked(row, !row->checked()); } base::unique_qptr InviteController::rowContextMenu( QWidget *parent, not_null row) { return nullptr; } void InviteController::itemDeselectedHook(not_null peer) { } bool InviteController::hasRowFor(not_null peer) const { return (delegate()->peerListFindRow(peer->id.value) != nullptr); } bool InviteController::isAlreadyIn(not_null user) const { return _alreadyIn.contains(user); } std::unique_ptr InviteController::createRow( not_null participant) const { const auto user = participant->asUser(); if (!user || user->isSelf() || user->isBot() || user->isInaccessible()) { return nullptr; } auto result = std::make_unique(user); _rowAdded.fire_copy(user); _inGroup.emplace(user); if (isAlreadyIn(user)) { result->setDisabledState(PeerListRow::State::DisabledChecked); } return result; } auto InviteController::peersWithRows() const -> not_null>*> { return &_inGroup; } rpl::producer> InviteController::rowAdded() const { return _rowAdded.events(); } InviteContactsController::InviteContactsController( not_null peer, base::flat_set> alreadyIn, not_null>*> inGroup, rpl::producer> 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 user) { if (auto row = delegate()->peerListFindRow(user->id.value)) { delegate()->peerListRemoveRow(row); } }, _lifetime); } std::unique_ptr InviteContactsController::createRow( not_null user) { return _inGroup->contains(user) ? nullptr : AddParticipantsBoxController::createRow(user); } object_ptr PrepareInviteBox( not_null call, Fn showToast, Fn 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>(); 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>(); const auto shareLink = [=] { Expects(shareConferenceLink != nullptr); shareConferenceLink(); (*close)(); }; auto controller = std::make_unique( &real->session(), ConfInviteDarkStyles(), alreadyIn, shareLink, std::vector>()); const auto raw = controller.get(); raw->setStyleOverrides( &st::groupCallInviteMembersList, &st::groupCallMultiSelect); auto initBox = [=](not_null 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(std::move(controller), initBox); } auto controller = std::make_unique(peer, alreadyIn); controller->setStyleOverrides( &st::groupCallInviteMembersList, &st::groupCallMultiSelect); auto contactsController = std::make_unique( peer, std::move(alreadyIn), controller->peersWithRows(), controller->rowAdded()); contactsController->setStyleOverrides( &st::groupCallInviteMembersList, &st::groupCallMultiSelect); const auto invite = [=](const std::vector> &users) { const auto call = weak.get(); if (!call) { return; } auto requests = ranges::views::all( users ) | ranges::views::transform([](not_null 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 show, const std::vector> &users, const std::vector> &nonMembers, Fn finish) { peer->session().api().chatParticipants().add( show, peer, nonMembers, true, [=](bool) { invite(users); finish(); }); }; const auto inviteWithConfirmation = [=]( not_null parentBox, const std::vector> &users, const std::vector> &nonMembers, Fn 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>(); 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 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 peer) { return not_null(peer->asUser()); }) | ranges::to_vector; const auto nonMembers = ranges::views::all( users ) | ranges::views::filter([&](not_null 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>(); controllers.push_back(std::move(controller)); controllers.push_back(std::move(contactsController)); return Box(std::move(controllers), initBox); } object_ptr PrepareInviteBox( not_null call, Fn)> inviteUsers, Fn shareLink) { const auto user = call->user(); const auto weak = base::make_weak(call); auto alreadyIn = base::flat_set>{ user }; auto controller = std::make_unique( &user->session(), ConfInviteDarkStyles(), alreadyIn, shareLink, std::vector>()); const auto raw = controller.get(); raw->setStyleOverrides( &st::groupCallInviteMembersList, &st::groupCallMultiSelect); auto initBox = [=](not_null 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(std::move(controller), initBox); } not_null CreateReActivateHeader(not_null parent) { const auto result = Ui::CreateChild(parent); result->add( MakeJoinCallLogo(result), st::boxRowPadding + st::confcallLinkHeaderIconPadding); result->add( object_ptr>( result, object_ptr( result, tr::lng_confcall_inactive_title(), st::boxTitle)), st::boxRowPadding + st::confcallLinkTitlePadding); result->add( object_ptr( result, tr::lng_confcall_inactive_about(), st::confcallLinkCenteredText), st::boxRowPadding + st::confcallLinkTitlePadding )->setTryMakeSimilarLines(true); Ui::AddDivider(result); return result; } void InitReActivate(not_null box) { box->setTitle(rpl::producer(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 PrepareInviteToEmptyBox( std::shared_ptr call, MsgId inviteMsgId, std::vector> prioritize) { auto controller = std::make_unique( &call->session(), ConfInviteDefaultStyles(), base::flat_set>(), nullptr, std::move(prioritize)); const auto raw = controller.get(); raw->setStyleOverrides(&st::createCallList); const auto initBox = [=](not_null 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(std::move(controller), initBox); } object_ptr PrepareCreateCallBox( not_null<::Window::SessionController*> window, Fn created, MsgId discardedInviteMsgId, std::vector> prioritize) { struct State { bool creatingLink = false; QPointer box; }; const auto state = std::make_shared(); 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( &window->session(), ConfInviteDefaultStyles(), base::flat_set>(), discardedInviteMsgId ? Fn() : shareLink, std::move(prioritize)); const auto raw = controller.get(); if (discardedInviteMsgId) { raw->setStyleOverrides(&st::createCallList); } const auto initBox = [=](not_null 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(std::move(controller), initBox); state->box = result.data(); return result; } } // namespace Calls::Group