From 6c65aae5ccefcdc06fb0bc831dcd068bedb0acb3 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 12 Jun 2026 09:49:47 +0400 Subject: [PATCH] Add pending requests box for chats to be added. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 19 + .../SourceFiles/boxes/peers/community_box.cpp | 27 + .../peers/community_pending_requests_box.cpp | 489 ++++++++++++++++++ .../peers/community_pending_requests_box.h | 18 + .../boxes/peers/manage_community_box.cpp | 24 + 6 files changed, 579 insertions(+) create mode 100644 Telegram/SourceFiles/boxes/peers/community_pending_requests_box.cpp create mode 100644 Telegram/SourceFiles/boxes/peers/community_pending_requests_box.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index d0fe567e1b..3c88730fd3 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -232,6 +232,8 @@ PRIVATE boxes/peers/choose_peer_box.h boxes/peers/community_box.cpp boxes/peers/community_box.h + boxes/peers/community_pending_requests_box.cpp + boxes/peers/community_pending_requests_box.h boxes/peers/create_managed_bot_box.cpp boxes/peers/create_managed_bot_box.h boxes/peers/edit_contact_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index ce9548cf6c..70e01102f1 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2240,6 +2240,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_community_chats_viewable" = "Chats you can view"; "lng_community_chats_requestable" = "Chats you can request to join"; "lng_community_add_chat" = "Add a Chat to Community"; +"lng_community_requests_title" = "Pending Requests"; +"lng_community_requests_about" = "Chats require admin approval before they're added to the community."; +"lng_community_requests_count#one" = "{count} chat suggested for this community"; +"lng_community_requests_count#other" = "{count} chats suggested for this community"; +"lng_community_requests_button#one" = "{count} Pending Request"; +"lng_community_requests_button#other" = "{count} Pending Requests"; +"lng_community_request_suggested_group" = "{user} suggested this group"; +"lng_community_request_suggested_channel" = "{user} suggested this channel"; +"lng_community_request_only_members" = "Only visible to its members"; +"lng_community_request_add" = "Add"; +"lng_community_request_decline" = "Decline"; +"lng_community_requests_add_all" = "Add All"; +"lng_community_requests_decline_all" = "Decline All"; +"lng_community_requests_add_all_title" = "Add All Chats"; +"lng_community_requests_add_all_sure#one" = "Do you want to add {count} chat to the community?"; +"lng_community_requests_add_all_sure#other" = "Do you want to add {count} chats to the community?"; +"lng_community_requests_decline_all_title" = "Decline All"; +"lng_community_requests_decline_all_sure#one" = "Do you want to decline {count} request to join the community?"; +"lng_community_requests_decline_all_sure#other" = "Do you want to decline all {count} requests to join the community?"; "lng_manage_monoforum" = "Direct Messages"; "lng_manage_monoforum_off" = "Off"; diff --git a/Telegram/SourceFiles/boxes/peers/community_box.cpp b/Telegram/SourceFiles/boxes/peers/community_box.cpp index d65fc9eb16..48eb69d100 100644 --- a/Telegram/SourceFiles/boxes/peers/community_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/community_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "boxes/peer_list_box.h" #include "boxes/peers/add_to_community_box.h" +#include "boxes/peers/community_pending_requests_box.h" #include "boxes/peers/manage_community_box.h" #include "data/data_changes.h" #include "data/data_channel.h" @@ -163,6 +164,32 @@ void CommunityBox( Ui::AddSkip(container); Ui::AddDividerText(container, tr::lng_community_show_as_one_about()); + if (community->canManageLinkedPeers()) { + const auto wrap = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto inner = wrap->entity(); + Ui::AddSkip(inner); + auto count = Info::Profile::PendingRequestsCountValue( + community + ) | rpl::start_spawning(wrap->lifetime()); + Settings::AddButtonWithIcon( + inner, + tr::lng_community_requests_button( + lt_count, + rpl::duplicate(count) | tr::to_count()), + st::settingsButton, + { &st::menuIconInvite } + )->addClickHandler([=] { + ShowCommunityPendingRequestsBox(navigation, community); + }); + wrap->toggleOn( + std::move(count) | rpl::map(rpl::mappers::_1 > 0), + anim::type::instant); + wrap->finishAnimating(); + } + auto chats = info->linkedPeersValue( ) | rpl::map([=] { return PartitionChats(info); diff --git a/Telegram/SourceFiles/boxes/peers/community_pending_requests_box.cpp b/Telegram/SourceFiles/boxes/peers/community_pending_requests_box.cpp new file mode 100644 index 0000000000..cf80d03451 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/community_pending_requests_box.cpp @@ -0,0 +1,489 @@ +/* +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/peers/community_pending_requests_box.h" + +#include "api/api_communities.h" +#include "apiwrap.h" +#include "boxes/peer_list_box.h" +#include "boxes/peers/manage_community_box.h" +#include "data/data_channel.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" +#include "ui/round_rect.h" +#include "ui/text/text_utilities.h" +#include "ui/vertical_list.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +namespace { + +constexpr auto kPerPage = 100; +constexpr auto kAcceptButton = 1; +constexpr auto kRejectButton = 2; + +class RowDelegate { +public: + [[nodiscard]] virtual QSize rowAcceptButtonSize() = 0; + [[nodiscard]] virtual QSize rowRejectButtonSize() = 0; + virtual void rowPaintAccept( + Painter &p, + QRect geometry, + std::unique_ptr &ripple, + int outerWidth, + bool over) = 0; + virtual void rowPaintReject( + Painter &p, + QRect geometry, + std::unique_ptr &ripple, + int outerWidth, + bool over) = 0; +}; + +class Row final : public PeerListRow { +public: + Row( + not_null delegate, + const Api::CommunityPeerRequest &request); + + 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: + const not_null _delegate; + std::unique_ptr _acceptRipple; + std::unique_ptr _rejectRipple; + +}; + +Row::Row( + not_null delegate, + const Api::CommunityPeerRequest &request) +: PeerListRow(request.peer) +, _delegate(delegate) { + const auto suggestedBy = request.requestedBy + ? request.requestedBy->shortName() + : QString(); + auto status = request.peer->isBroadcast() + ? tr::lng_community_request_suggested_channel( + tr::now, + lt_user, + suggestedBy) + : tr::lng_community_request_suggested_group( + tr::now, + lt_user, + suggestedBy); + if (!request.visible) { + status += QString::fromUtf8(" \xE2\x80\xA2 ") + + tr::lng_community_request_only_members(tr::now); + } + setCustomStatus(status); +} + +int Row::elementsCount() const { + return 2; +} + +QRect Row::elementGeometry(int element, int outerWidth) const { + switch (element) { + case kAcceptButton: { + const auto size = _delegate->rowAcceptButtonSize(); + return QRect(st::requestAcceptPosition, size); + } break; + case kRejectButton: { + const auto accept = _delegate->rowAcceptButtonSize(); + const auto size = _delegate->rowRejectButtonSize(); + return QRect( + (st::requestAcceptPosition + + QPoint(accept.width() + st::requestButtonsSkip, 0)), + size); + } break; + } + return QRect(); +} + +bool Row::elementDisabled(int element) const { + return false; +} + +bool Row::elementOnlySelect(int element) const { + return true; +} + +void Row::elementAddRipple( + int element, + QPoint point, + Fn updateCallback) { + const auto pointer = (element == kAcceptButton) + ? &_acceptRipple + : (element == kRejectButton) + ? &_rejectRipple + : nullptr; + if (!pointer) { + return; + } + auto &ripple = *pointer; + if (!ripple) { + auto mask = Ui::RippleAnimation::RoundRectMask( + (element == kAcceptButton + ? _delegate->rowAcceptButtonSize() + : _delegate->rowRejectButtonSize()), + st::buttonRadius); + ripple = std::make_unique( + (element == kAcceptButton + ? st::requestsAcceptButton.ripple + : st::requestsRejectButton.ripple), + std::move(mask), + std::move(updateCallback)); + } + ripple->add(point); +} + +void Row::elementsStopLastRipple() { + if (_acceptRipple) { + _acceptRipple->lastStop(); + } + if (_rejectRipple) { + _rejectRipple->lastStop(); + } +} + +void Row::elementsPaint( + Painter &p, + int outerWidth, + bool selected, + int selectedElement) { + const auto accept = elementGeometry(kAcceptButton, outerWidth); + const auto reject = elementGeometry(kRejectButton, outerWidth); + + const auto over = [&](int element) { + return (selectedElement == element); + }; + _delegate->rowPaintAccept( + p, + accept, + _acceptRipple, + outerWidth, + over(kAcceptButton)); + _delegate->rowPaintReject( + p, + reject, + _rejectRipple, + outerWidth, + over(kRejectButton)); +} + +class Controller final + : public PeerListController + , public RowDelegate + , public base::has_weak_ptr { +public: + Controller( + not_null navigation, + not_null community); + + Main::Session &session() const override; + void prepare() override; + void loadMoreRows() override; + void rowClicked(not_null row) override; + void rowElementClicked( + not_null row, + int element) override; + + QSize rowAcceptButtonSize() override; + QSize rowRejectButtonSize() override; + void rowPaintAccept( + Painter &p, + QRect geometry, + std::unique_ptr &ripple, + int outerWidth, + bool over) override; + void rowPaintReject( + Painter &p, + QRect geometry, + std::unique_ptr &ripple, + int outerWidth, + bool over) override; + +private: + void paintButton( + Painter &p, + QRect geometry, + const style::RoundButton &st, + const Ui::RoundRect &rect, + const Ui::RoundRect &rectOver, + std::unique_ptr &ripple, + const QString &text, + int textWidth, + int outerWidth, + bool over); + void process(not_null row, bool reject); + + const not_null _navigation; + const not_null _community; + + QString _offset; + bool _allLoaded = false; + bool _loading = false; + + Ui::RoundRect _acceptRect; + Ui::RoundRect _acceptRectOver; + Ui::RoundRect _rejectRect; + Ui::RoundRect _rejectRectOver; + QString _acceptText; + QString _rejectText; + int _acceptTextWidth = 0; + int _rejectTextWidth = 0; + +}; + +Controller::Controller( + not_null navigation, + not_null community) +: _navigation(navigation) +, _community(community) +, _acceptRect(st::buttonRadius, st::requestsAcceptButton.textBg) +, _acceptRectOver(st::buttonRadius, st::requestsAcceptButton.textBgOver) +, _rejectRect(st::buttonRadius, st::requestsRejectButton.textBg) +, _rejectRectOver(st::buttonRadius, st::requestsRejectButton.textBgOver) +, _acceptText(tr::lng_community_request_add(tr::now)) +, _rejectText(tr::lng_community_request_decline(tr::now)) +, _acceptTextWidth(st::requestsAcceptButton.style.font->width(_acceptText)) +, _rejectTextWidth(st::requestsRejectButton.style.font->width(_rejectText)) { + setStyleOverrides(&st::requestsBoxList); +} + +Main::Session &Controller::session() const { + return _community->session(); +} + +void Controller::prepare() { + delegate()->peerListSetTitle(tr::lng_community_requests_title()); + loadMoreRows(); +} + +void Controller::loadMoreRows() { + if (_allLoaded || _loading) { + return; + } + _loading = true; + session().api().communities().requestPeerLinkRequests( + _community, + _offset, + kPerPage, + crl::guard(this, [=](Api::CommunityPeerRequestsSlice slice) { + _loading = false; + for (const auto &request : slice.list) { + if (!delegate()->peerListFindRow(request.peer->id.value)) { + delegate()->peerListAppendRow( + std::make_unique(this, request)); + } + } + delegate()->peerListRefreshRows(); + _offset = slice.nextOffset; + if (_offset.isEmpty()) { + _allLoaded = true; + } + })); +} + +void Controller::rowClicked(not_null row) { + const auto peer = row->peer(); + if (const auto window = _navigation->parentController()) { + window->showPeer(peer); + } +} + +void Controller::rowElementClicked( + not_null row, + int element) { + if (element == kAcceptButton) { + process(row, false); + } else if (element == kRejectButton) { + process(row, true); + } +} + +void Controller::process(not_null row, bool reject) { + const auto peer = row->peer(); + const auto id = peer->id.value; + session().api().communities().togglePeerLinkRequestApproval( + _community, + peer, + reject, + crl::guard(this, [=] { + if (const auto row = delegate()->peerListFindRow(id)) { + delegate()->peerListRemoveRow(row); + delegate()->peerListRefreshRows(); + } + }), + crl::guard(this, [=](const QString &error) { + delegate()->peerListUiShow()->showToast(error); + })); +} + +QSize Controller::rowAcceptButtonSize() { + return QSize( + _acceptTextWidth + st::requestsAcceptButton.width, + st::requestsAcceptButton.height); +} + +QSize Controller::rowRejectButtonSize() { + return QSize( + _rejectTextWidth + st::requestsRejectButton.width, + st::requestsRejectButton.height); +} + +void Controller::rowPaintAccept( + Painter &p, + QRect geometry, + std::unique_ptr &ripple, + int outerWidth, + bool over) { + paintButton( + p, + geometry, + st::requestsAcceptButton, + _acceptRect, + _acceptRectOver, + ripple, + _acceptText, + _acceptTextWidth, + outerWidth, + over); +} + +void Controller::rowPaintReject( + Painter &p, + QRect geometry, + std::unique_ptr &ripple, + int outerWidth, + bool over) { + paintButton( + p, + geometry, + st::requestsRejectButton, + _rejectRect, + _rejectRectOver, + ripple, + _rejectText, + _rejectTextWidth, + outerWidth, + over); +} + +void Controller::paintButton( + Painter &p, + QRect geometry, + const style::RoundButton &st, + const Ui::RoundRect &rect, + const Ui::RoundRect &rectOver, + std::unique_ptr &ripple, + const QString &text, + int textWidth, + int outerWidth, + bool over) { + rect.paint(p, geometry); + if (over) { + rectOver.paint(p, geometry); + } + if (ripple) { + ripple->paint(p, geometry.x(), geometry.y(), outerWidth); + if (ripple->empty()) { + ripple = nullptr; + } + } + p.setFont(st.style.font); + p.setPen(over ? st.textFgOver : st.textFg); + p.drawTextLeft( + geometry.x() + (geometry.width() - textWidth) / 2, + geometry.y() + st.textTop, + outerWidth, + text); +} + +} // namespace + +void ShowCommunityPendingRequestsBox( + not_null navigation, + not_null community) { + auto controller = std::make_unique(navigation, community); + const auto init = [=](not_null box) { + const auto count = std::max(community->pendingRequestsCount(), 1); + const auto processAll = [=](bool reject) { + const auto sure = [=](Fn &&close) { + close(); + community->session().api().communities( + ).toggleAllPeerLinkRequestApproval( + community, + reject, + crl::guard(box, [=] { box->closeBox(); }), + crl::guard(box, [=](const QString &error) { + box->uiShow()->showToast(error); + })); + }; + box->uiShow()->showBox(Ui::MakeConfirmBox({ + .text = (reject + ? tr::lng_community_requests_decline_all_sure( + tr::now, + lt_count, + count) + : tr::lng_community_requests_add_all_sure( + tr::now, + lt_count, + count)), + .confirmed = sure, + .confirmText = (reject + ? tr::lng_community_request_decline() + : tr::lng_community_request_add()), + .title = (reject + ? tr::lng_community_requests_decline_all_title() + : tr::lng_community_requests_add_all_title()), + })); + }; + box->addButton( + tr::lng_community_requests_add_all(), + [=] { processAll(false); }); + box->addLeftButton( + tr::lng_community_requests_decline_all(), + [=] { processAll(true); }); + + auto above = object_ptr(box); + Ui::AddDividerText(above, tr::lng_community_requests_about()); + Ui::AddSkip(above); + Ui::AddSubsectionTitle( + above, + tr::lng_community_requests_count( + lt_count, + rpl::single(float64(count)))); + box->peerListSetAboveWidget(std::move(above)); + }; + navigation->uiShow()->showBox(Box( + std::move(controller), + init)); +} diff --git a/Telegram/SourceFiles/boxes/peers/community_pending_requests_box.h b/Telegram/SourceFiles/boxes/peers/community_pending_requests_box.h new file mode 100644 index 0000000000..f38d35f022 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/community_pending_requests_box.h @@ -0,0 +1,18 @@ +/* +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; + +namespace Window { +class SessionNavigation; +} // namespace Window + +void ShowCommunityPendingRequestsBox( + not_null navigation, + not_null community); diff --git a/Telegram/SourceFiles/boxes/peers/manage_community_box.cpp b/Telegram/SourceFiles/boxes/peers/manage_community_box.cpp index 93b9d43a1c..587079e903 100644 --- a/Telegram/SourceFiles/boxes/peers/manage_community_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/manage_community_box.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/manage_community_box.h" #include "apiwrap.h" +#include "boxes/peers/community_pending_requests_box.h" #include "boxes/peers/edit_participants_box.h" #include "data/data_changes.h" #include "data/data_channel.h" @@ -23,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" +#include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" @@ -179,6 +181,28 @@ void ManageCommunityBox( community, ParticipantsBoxController::Role::Admins); }); + if (community->canManageLinkedPeers()) { + const auto wrap = container->add( + object_ptr>( + container, + object_ptr(container))); + auto count = Info::Profile::PendingRequestsCountValue( + community + ) | rpl::start_spawning(wrap->lifetime()); + Settings::AddButtonWithLabel( + wrap->entity(), + tr::lng_community_requests_title(), + PositiveNumberString(rpl::duplicate(count)), + st::settingsButton, + { &st::menuIconInvite } + )->addClickHandler([=] { + ShowCommunityPendingRequestsBox(navigation, community); + }); + wrap->toggleOn( + std::move(count) | rpl::map(rpl::mappers::_1 > 0), + anim::type::instant); + wrap->finishAnimating(); + } Settings::AddButtonWithLabel( container, tr::lng_manage_peer_removed_users(),