diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 85b5a98844..ca6cec7c8a 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -325,6 +325,8 @@ PRIVATE boxes/report_messages_box.h boxes/ringtones_box.cpp boxes/ringtones_box.h + boxes/select_future_owner_box.cpp + boxes/select_future_owner_box.h boxes/self_destruction_box.cpp boxes/self_destruction_box.h boxes/send_credits_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 687c68f4c1..270755cfea 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1643,6 +1643,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_invite_upgrade_via_channel_about" = "You can send an invite link to the channel in a private message instead."; "lng_invite_status_disabled" = "available only to Premium users"; +"lng_leave_next_owner_box_title" = "Leave Channel?"; +"lng_leave_next_owner_box_title_group" = "Leave Group?"; +"lng_leave_next_owner_box_about" = "If you leave, **{user}** will become the new owner of **{chat}** in **1 week**."; +"lng_leave_next_owner_box_about_admin" = "If you leave, **{user}** will become the new admin of **{chat}** in **1 week**."; +"lng_select_next_owner_box" = "Appoint Another Owner"; +"lng_select_next_owner_box_admin" = "Appoint Another Admin"; +"lng_select_next_owner_box_title" = "Appoint New Owner"; +"lng_select_next_owner_box_title_admin" = "Appoint New Admin"; +"lng_select_next_owner_box_confirm" = "Appoint and Leave Group"; +"lng_select_next_owner_box_sub_admins" = "Channel admins"; +"lng_select_next_owner_box_sub_admins_group" = "Group admins"; +"lng_select_next_owner_box_sub_members" = "Channel members"; +"lng_select_next_owner_box_sub_members_group" = "Group members"; +"lng_select_next_owner_box_status_joined" = "joined {date}"; +"lng_select_next_owner_box_status_promoted" = "promoted {date}"; + "lng_via_link_group_one" = "**{user}** restricts adding them to groups.\nYou can send them an invite link as message instead."; "lng_via_link_group_many#one" = "**{count} user** restricts adding them to groups.\nYou can send them an invite link as message instead."; "lng_via_link_group_many#other" = "**{count} users** restrict adding them to groups.\nYou can send them an invite link as message instead."; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index dfdc48c4be..f12f528fe4 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1182,3 +1182,9 @@ moderateCommonGroupsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) { }}; } } + +futureOwnerBox: Box(defaultBox) { + buttonPadding: margins(0px, 0px, 0px, 14px); + buttonHeight: 0px; +} +futureOwnerBoxSelect: collectibleBox; diff --git a/Telegram/SourceFiles/boxes/select_future_owner_box.cpp b/Telegram/SourceFiles/boxes/select_future_owner_box.cpp new file mode 100644 index 0000000000..b3b12622e5 --- /dev/null +++ b/Telegram/SourceFiles/boxes/select_future_owner_box.cpp @@ -0,0 +1,402 @@ +/* +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 "boxes/select_future_owner_box.h" + +#include "api/api_chat_participants.h" +#include "api/api_cloud_password.h" +#include "apiwrap.h" +#include "base/unixtime.h" +#include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/passcode_box.h" +#include "boxes/peer_list_controllers.h" +#include "boxes/peer_lists_box.h" +#include "boxes/peers/replace_boost_box.h" // CreateUserpicsTransfer. +#include "core/application.h" +#include "core/core_cloud_password.h" +#include "data/data_channel.h" +#include "boxes/peers/channel_ownership_transfer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "info/profile/info_profile_values.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/layers/generic_box.h" +#include "ui/text/format_values.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +namespace { + +enum class ParticipantType { + Admins, + Members +}; + +class ParticipantsController : public PeerListController { +public: + ParticipantsController( + not_null window, + not_null channel, + ParticipantType type); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void loadMoreRows() override; + void itemDeselectedHook(not_null peer) override; + + void setOnRowClicked(Fn callback); + rpl::producer<> itemDeselected() const; + +private: + const not_null _window; + const not_null _channel; + const ParticipantType _type; + MTP::Sender _api; + Fn _onRowClicked; + rpl::event_stream<> _itemDeselected; + + mtpRequestId _loadRequestId = 0; + int _offset = 0; + bool _allLoaded = false; +}; + +ParticipantsController::ParticipantsController( + not_null window, + not_null channel, + ParticipantType type) +: _window(window) +, _channel(channel) +, _type(type) +, _api(&channel->session().mtp()) { +} + +Main::Session &ParticipantsController::session() const { + return _channel->session(); +} + +void ParticipantsController::setOnRowClicked(Fn callback) { + _onRowClicked = callback; +} + +void ParticipantsController::prepare() { + loadMoreRows(); +} + +void ParticipantsController::rowClicked(not_null row) { + delegate()->peerListSetRowChecked( + row, + !delegate()->peerListIsRowChecked(row)); + for (auto i = 0; i < delegate()->peerListFullRowsCount(); ++i) { + auto r = delegate()->peerListRowAt(i); + if (r != row) { + delegate()->peerListSetRowChecked(r, false); + } + } + if (_onRowClicked) { + _onRowClicked(); + } +} + +void ParticipantsController::itemDeselectedHook(not_null peer) { + _itemDeselected.fire({}); +} + +rpl::producer<> ParticipantsController::itemDeselected() const { + return _itemDeselected.events(); +} + +void ParticipantsController::loadMoreRows() { + if (_loadRequestId || _allLoaded) { + return; + } + + const auto perPage = (_offset > 0) ? 200 : 50; + const auto participantsHash = uint64(0); + const auto filter = (_type == ParticipantType::Admins) + ? MTP_channelParticipantsAdmins() + : MTP_channelParticipantsRecent(); + + _loadRequestId = _api.request(MTPchannels_GetParticipants( + _channel->inputChannel(), + filter, + MTP_int(_offset), + MTP_int(perPage), + MTP_long(participantsHash) + )).done([=](const MTPchannels_ChannelParticipants &result) { + _loadRequestId = 0; + auto added = false; + + result.match([&](const MTPDchannels_channelParticipants &data) { + const auto &[availableCount, list] = Api::ChatParticipants::Parse( + _channel, + data); + for (const auto &participant : list) { + if (const auto user = _channel->owner().userLoaded( + participant.userId())) { + if (delegate()->peerListFindRow(user->id.value)) { + continue; + } + if (user->isBot()) { + continue; + } + using Type = Api::ChatParticipant::Type; + if ((participant.type() == Type::Creator) + || (_type == ParticipantType::Members + && (participant.type() == Type::Admin))) { + continue; + } + auto row = std::make_unique(user); + const auto promotedSince = participant.promotedSince(); + row->setCustomStatus( + (promotedSince + ? tr::lng_select_next_owner_box_status_promoted + : tr::lng_select_next_owner_box_status_joined)( + tr::now, + lt_date, + Ui::FormatDateTime( + base::unixtime::parse(promotedSince + ? promotedSince + : participant.memberSince())))); + delegate()->peerListAppendRow(std::move(row)); + added = true; + } + } + if (const auto size = list.size()) { + _offset += size; + } else { + _allLoaded = true; + } + }, [](const MTPDchannels_channelParticipantsNotModified &) { + }); + + if (!added && _offset > 0) { + _allLoaded = true; + } + delegate()->peerListRefreshRows(); + }).fail([=] { + _loadRequestId = 0; + }).send(); +} + +} // namespace + +void SelectFutureOwnerbox( + not_null box, + not_null channel, + not_null user) { + const auto content = box->verticalLayout(); + Ui::AddSkip(content); + Ui::AddSkip(content); + content->add( + CreateUserpicsTransfer( + content, + rpl::single(std::vector>{ + user->session().user(), + channel, + }), + user, + UserpicsTransferType::ChannelFutureOwner), + st::boxRowPadding); + Ui::AddSkip(content); + Ui::AddSkip(content); + const auto title = content->add( + object_ptr( + content, + channel->isMegagroup() + ? tr::lng_leave_next_owner_box_title_group() + : tr::lng_leave_next_owner_box_title(), + box->getDelegate()->style().title), + st::boxRowPadding); + Ui::AddSkip(content); + Ui::AddSkip(content); + const auto adminsAreEqual = false; + const auto about = content->add( + object_ptr( + content, + (adminsAreEqual + ? tr::lng_leave_next_owner_box_about + : tr::lng_leave_next_owner_box_about_admin)( + lt_user, + Info::Profile::NameValue(user) | rpl::map(tr::marked), + lt_chat, + Info::Profile::NameValue(channel) | rpl::map(tr::marked), + tr::rich), + st::boxLabel), + st::boxRowPadding); + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddSkip(content); + + const auto select = content->add( + object_ptr( + content, + !adminsAreEqual + ? tr::lng_select_next_owner_box() + : tr::lng_select_next_owner_box_admin(), + st::defaultLightButton), + st::boxRowPadding, + style::al_justify); + Ui::AddSkip(content); + const auto cancel = content->add( + object_ptr( + content, + tr::lng_cancel(), + st::defaultLightButton), + st::boxRowPadding, + style::al_justify); + cancel->setClickedCallback([=] { + box->closeBox(); + }); + Ui::AddSkip(content); + const auto leave = content->add( + object_ptr( + content, + channel->isMegagroup() + ? tr::lng_profile_leave_group() + : tr::lng_profile_leave_channel(), + st::attentionBoxButton), + st::boxRowPadding, + style::al_justify); + leave->setClickedCallback([=, revoke = false] { + channel->session().api().deleteConversation(channel, revoke); + box->closeBox(); + }); + select->setClickedCallback([=] { + const auto window = Core::App().findWindow(box); + const auto sessionController = window + ? window->sessionController() + : nullptr; + if (!sessionController) { + return; + } + + auto adminsOwned = std::make_unique( + sessionController, + channel, + ParticipantType::Admins); + + auto membersOwned = std::make_unique( + sessionController, + channel, + ParticipantType::Members); + + const auto admins = adminsOwned.get(); + const auto members = membersOwned.get(); + + auto initBox = [=](not_null selectBox) { + const auto selectionChanges + = selectBox->lifetime().make_state>(); + const auto uncheckOtherList = [=]( + not_null otherController) { + auto delegate = otherController->delegate(); + const auto full = delegate->peerListFullRowsCount(); + for (auto i = 0; i < full; ++i) { + delegate->peerListSetRowChecked( + delegate->peerListRowAt(i), + false); + } + selectionChanges->fire({}); + }; + admins->setOnRowClicked([=] { uncheckOtherList(members); }); + members->setOnRowClicked([=] { uncheckOtherList(admins); }); + selectBox->setTitle(!adminsAreEqual + ? tr::lng_select_next_owner_box_title() + : tr::lng_select_next_owner_box_title_admin()); + const auto searchEnabled = PeerListSearchMode::Enabled; + admins->delegate()->peerListSetSearchMode(searchEnabled); + members->delegate()->peerListSetSearchMode(searchEnabled); + rpl::merge( + admins->itemDeselected(), + members->itemDeselected() + ) | rpl::on_next([=] { + selectionChanges->fire({}); + }, selectBox->lifetime()); + selectBox->addSeparatorBefore( + 0, + CreatePeerListSectionSubtitle( + selectBox, + !channel->isMegagroup() + ? tr::lng_select_next_owner_box_sub_admins() + : tr::lng_select_next_owner_box_sub_admins_group())); + selectBox->addSeparatorBefore( + 1, + CreatePeerListSectionSubtitle( + selectBox, + !channel->isMegagroup() + ? tr::lng_select_next_owner_box_sub_members() + : tr::lng_select_next_owner_box_sub_members_group())); + { + const auto &st = st::futureOwnerBoxSelect; + selectBox->setStyle(st); + auto button = object_ptr( + selectBox, + rpl::conditional( + selectionChanges->events( + ) | rpl::map([=] { + return !selectBox->collectSelectedRows().empty(); + }), + tr::lng_select_next_owner_box_confirm(), + tr::lng_close()), + st::defaultActiveButton); + button->setTextTransform( + Ui::RoundButton::TextTransform::NoTransform); + const auto raw = button.data(); + rpl::combine( + selectionChanges->events() | rpl::map_to(0), + selectBox->widthValue() + ) | rpl::on_next([=](int, int width) { + raw->resizeToWidth(width + - st.buttonPadding.left() + - st.buttonPadding.right()); + }, selectBox->lifetime()); + button->setFullRadius(true); + button->setClickedCallback([=] { + const auto selected = selectBox->collectSelectedRows(); + if (selected.empty()) { + return selectBox->closeBox(); + } + if (const auto user = selected.front()->asUser()) { + auto &lifetime = selectBox->lifetime(); + lifetime.make_state( + channel, + user, + selectBox->uiShow(), + [=](std::shared_ptr show) { + const auto revoke = false; + channel->session().api().deleteConversation( + channel, + revoke); + show->hideLayer(); + })->start(); + } + }); + selectBox->addButton(std::move(button)); + selectionChanges->fire({}); + } + }; + + auto controllers = std::vector>(); + controllers.reserve(2); + controllers.push_back(std::move(adminsOwned)); + controllers.push_back(std::move(membersOwned)); + box->uiShow()->showBox( + Box(std::move(controllers), initBox)); + }); + for (const auto &b : { select, cancel, leave }) { + b->setFullRadius(true); + b->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + } + box->setStyle(st::futureOwnerBox); +} \ No newline at end of file diff --git a/Telegram/SourceFiles/boxes/select_future_owner_box.h b/Telegram/SourceFiles/boxes/select_future_owner_box.h new file mode 100644 index 0000000000..7f48bf64c1 --- /dev/null +++ b/Telegram/SourceFiles/boxes/select_future_owner_box.h @@ -0,0 +1,22 @@ +/* +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 +*/ +#pragma once + +class ChannelData; +class PeerData; +class UserData; + +namespace Ui { +class GenericBox; +class Show; +} // namespace Ui + +void SelectFutureOwnerbox( + not_null box, + not_null channel, + not_null user); diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp index 8c826f727c..547c54a854 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp @@ -967,11 +967,8 @@ void TopBar::setupActions(not_null controller) { this, tr::lng_profile_action_short_leave(tr::now), st::infoProfileTopBarActionLeave); - leaveButton->setClickedCallback([=] { - if (!controller->showFrozenError()) { - controller->show(Box(DeleteChatBox, peer)); - } - }); + leaveButton->setClickedCallback( + Window::DeleteAndLeaveHandler(controller, peer)); _actions->add(leaveButton); buttons.push_back(leaveButton); } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 9bebac443a..5fd8557386 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/delete_messages_box.h" #include "boxes/max_invite_box.h" #include "boxes/moderate_messages_box.h" +#include "boxes/select_future_owner_box.h" #include "boxes/choose_filter_box.h" #include "boxes/create_poll_box.h" #include "boxes/edit_todo_list_box.h" @@ -3655,6 +3656,26 @@ Fn ClearHistoryHandler( Fn DeleteAndLeaveHandler( not_null controller, not_null peer) { + if (const auto channel = peer->asChannel(); + channel && channel->amCreator()) { + const auto requestId = std::make_shared(0); + return [=] { + if (controller->showFrozenError() || (*requestId > 0)) { + return; + } + *requestId = peer->session().api().request( + MTPchannels_GetFutureCreatorAfterLeave( + channel->inputChannel() + )).done([=](const MTPUser &result) { + *requestId = 0; + const auto user = peer->owner().processUser(result); + controller->show(Box(SelectFutureOwnerbox, channel, user)); + }).fail([=](const MTP::Error &error) { + *requestId = 0; + controller->show(Box(DeleteChatBox, peer)); + }).send(); + }; + } return [=] { if (!controller->showFrozenError()) { controller->show(Box(DeleteChatBox, peer));