From b4895ef73065892c084dafb72fc32199678e0f37 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 12 Oct 2021 22:44:35 +0400 Subject: [PATCH] Allow to accept / reject requests by link. --- Telegram/SourceFiles/api/api_invite_links.cpp | 75 ++++- Telegram/SourceFiles/api/api_invite_links.h | 18 ++ .../boxes/peers/edit_peer_invite_link.cpp | 261 ++++++++++++++++-- .../boxes/peers/edit_peer_invite_links.cpp | 10 + .../SourceFiles/ui/boxes/edit_invite_link.cpp | 4 +- 5 files changed, 349 insertions(+), 19 deletions(-) diff --git a/Telegram/SourceFiles/api/api_invite_links.cpp b/Telegram/SourceFiles/api/api_invite_links.cpp index 1094be9d5..b2661bf8e 100644 --- a/Telegram/SourceFiles/api/api_invite_links.cpp +++ b/Telegram/SourceFiles/api/api_invite_links.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_changes.h" #include "main/main_session.h" +#include "base/unixtime.h" #include "apiwrap.h" namespace Api { @@ -435,6 +436,77 @@ void InviteLinks::requestMyLinks(not_null peer) { _firstSliceRequests.emplace(peer, requestId); } +void InviteLinks::processRequest( + not_null peer, + const QString &link, + not_null user, + bool approved, + Fn done, + Fn fail) { + if (_processRequests.contains({ peer, user })) { + return; + } + _processRequests.emplace( + std::pair{ peer, user }, + ProcessRequest{ std::move(done), std::move(fail) }); + using Flag = MTPmessages_HideChatJoinRequest::Flag; + _api->request(MTPmessages_HideChatJoinRequest( + MTP_flags(approved ? Flag::f_approved : Flag(0)), + peer->input, + user->inputUser + )).done([=](const MTPUpdates &result) { + if (const auto chat = peer->asChat()) { + if (chat->count > 0) { + if (chat->participants.size() >= chat->count) { + chat->participants.emplace(user); + } + ++chat->count; + } + } else if (const auto channel = peer->asChannel()) { + _api->requestParticipantsCountDelayed(channel); + } + _api->applyUpdates(result); + if (approved) { + const auto i = _firstJoined.find({ peer, link }); + if (i != end(_firstJoined)) { + ++i->second.count; + i->second.users.insert( + begin(i->second.users), + JoinedByLinkUser{ user, base::unixtime::now() }); + } + } + if (const auto callbacks = _processRequests.take({ peer, user })) { + if (const auto &done = callbacks->done) { + done(); + } + } + }).fail([=](const MTP::Error &error) { + if (const auto callbacks = _processRequests.take({ peer, user })) { + if (const auto &fail = callbacks->fail) { + fail(); + } + } + }).send(); +} + +void InviteLinks::applyExternalUpdate( + not_null peer, + InviteLink updated) { + if (const auto i = _firstSlices.find(peer); i != end(_firstSlices)) { + for (auto &link : i->second.links) { + if (link.link == updated.link) { + link = updated; + } + } + } + _updates.fire({ + .peer = peer, + .admin = updated.admin, + .was = updated.link, + .now = updated, + }); +} + std::optional InviteLinks::lookupJoinedFirstSlice( LinkKey key) const { const auto i = _firstJoined.find(key); @@ -498,7 +570,7 @@ void InviteLinks::requestJoinedFirstSlice(LinkKey key) { return; } const auto requestId = _api->request(MTPmessages_GetChatInviteImporters( - MTP_flags(0), + MTP_flags(MTPmessages_GetChatInviteImporters::Flag::f_link), key.peer->input, MTP_string(key.link), MTPstring(), // q @@ -645,6 +717,7 @@ auto InviteLinks::parse( .expireDate = data.vexpire_date().value_or_empty(), .usageLimit = data.vusage_limit().value_or_empty(), .usage = data.vusage().value_or_empty(), + .requested = data.vrequested().value_or_empty(), .requestApproval = data.is_request_needed(), .permanent = data.is_permanent(), .revoked = data.is_revoked(), diff --git a/Telegram/SourceFiles/api/api_invite_links.h b/Telegram/SourceFiles/api/api_invite_links.h index 24c22889f..0ade69856 100644 --- a/Telegram/SourceFiles/api/api_invite_links.h +++ b/Telegram/SourceFiles/api/api_invite_links.h @@ -19,6 +19,7 @@ struct InviteLink { TimeId expireDate = 0; int usageLimit = 0; int usage = 0; + int requested = 0; bool requestApproval = false; bool permanent = false; bool revoked = false; @@ -100,6 +101,15 @@ public: void requestMyLinks(not_null peer); [[nodiscard]] const Links &myLinks(not_null peer) const; + void processRequest( + not_null peer, + const QString &link, + not_null user, + bool approved, + Fn done, + Fn fail); + void applyExternalUpdate(not_null peer, InviteLink updated); + [[nodiscard]] rpl::producer joinedFirstSliceValue( not_null peer, const QString &link, @@ -136,6 +146,10 @@ private: return (a.peer == b.peer) && (a.link == b.link); } }; + struct ProcessRequest { + Fn done; + Fn fail; + }; [[nodiscard]] Links parseSlice( not_null peer, @@ -199,6 +213,10 @@ private: not_null, std::vector>> _deleteRevokedCallbacks; + base::flat_map< + std::pair, not_null>, + ProcessRequest> _processRequests; + rpl::event_stream _updates; struct AllRevokedDestroyed { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 84eb11e7f..1041de6cd 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -57,30 +57,109 @@ constexpr auto kShareQrPadding = 16; using LinkData = Api::InviteLink; -class Controller final : public PeerListController { +class RequestedRow final : public PeerListRow { public: + explicit RequestedRow(not_null peer); + + QSize actionSize() const override; + QMargins actionMargins() const override; + void paintAction( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + +}; + +RequestedRow::RequestedRow(not_null peer) +: PeerListRow(peer) { +} + +QSize RequestedRow::actionSize() const { + return QSize( + st::inviteLinkThreeDotsIcon.width(), + st::inviteLinkThreeDotsIcon.height()); +} + +QMargins RequestedRow::actionMargins() const { + return QMargins( + 0, + (st::inviteLinkList.item.height - actionSize().height()) / 2, + st::inviteLinkThreeDotsSkip, + 0); +} + +void RequestedRow::paintAction( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + (actionSelected + ? st::inviteLinkThreeDotsIconOver + : st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth); +} + +class Controller final + : public PeerListController + , public base::has_weak_ptr { +public: + enum class Role { + Requested, + Joined, + }; Controller( not_null peer, not_null admin, - rpl::producer data); + rpl::producer data, + Role role); void prepare() override; void loadMoreRows() override; void rowClicked(not_null row) override; + void rowActionClicked(not_null row) override; Main::Session &session() const override; rpl::producer boxHeightValue() const override; int descriptionTopSkipMin() const override; + struct Processed { + not_null user; + bool approved = false; + }; + [[nodiscard]] rpl::producer processed() const { + return _processed.events(); + } + private: + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + + void setupAboveJoinedWidget(); void appendSlice(const Api::JoinedByLinkSlice &slice); void addHeaderBlock(not_null container); + not_null*> addRequestedListBlock( + not_null container); + void updateWithProcessed(Processed processed); [[nodiscard]] rpl::producer dataValue() const; + [[nodiscard]] base::unique_qptr createRowContextMenu( + QWidget *parent, + not_null row); + void processRequest(not_null user, bool approved); + const not_null _peer; + const Role _role = Role::Joined; rpl::variable _data; + base::unique_qptr _menu; + rpl::event_stream _processed; + QString _link; bool _revoked = false; @@ -220,10 +299,12 @@ void QrBox( Controller::Controller( not_null peer, not_null admin, - rpl::producer data) + rpl::producer data, + Role role) : _peer(peer) +, _role(role) , _data(LinkData{ .admin = admin }) -, _api(&_peer->session().api().instance()) { +, _api(&session().api().instance()) { _data = std::move(data); const auto current = _data.current(); _link = current.link; @@ -386,7 +467,87 @@ void Controller::addHeaderBlock(not_null container) { }, lifetime()); } +not_null*> Controller::addRequestedListBlock( + not_null container) { + using namespace Settings; + + auto result = container->add( + object_ptr>( + container, + object_ptr( + container))); + const auto wrap = result->entity(); + // Make this container occupy full width. + wrap->add(object_ptr(wrap)); + AddDivider(wrap); + AddSkip(wrap); + auto requestedCount = dataValue( + ) | rpl::filter([](const LinkData &data) { + return data.requested > 0; + }) | rpl::map([=](const LinkData &data) { + return float64(data.requested); + }); + AddSubsectionTitle( + wrap, + tr::lng_group_invite_requested_full( + lt_count_decimal, + std::move(requestedCount))); + + const auto delegate = container->lifetime().make_state< + PeerListContentDelegateSimple + >(); + const auto controller = container->lifetime().make_state< + Controller + >(_peer, _data.current().admin, _data.value(), Role::Requested); + const auto content = container->add(object_ptr( + container, + controller)); + delegate->setContent(content); + controller->setDelegate(delegate); + + controller->processed( + ) | rpl::start_with_next([=](Processed processed) { + updateWithProcessed(processed); + }, lifetime()); + + return result; +} + void Controller::prepare() { + if (_role == Role::Joined) { + setupAboveJoinedWidget(); + + _allLoaded = (_data.current().usage == 0); + + const auto &inviteLinks = session().api().inviteLinks(); + const auto slice = inviteLinks.joinedFirstSliceLoaded(_peer, _link); + if (slice) { + appendSlice(*slice); + } + } else { + _allLoaded = (_data.current().requested == 0); + } + loadMoreRows(); +} + +void Controller::updateWithProcessed(Processed processed) { + const auto user = processed.user; + auto updated = _data.current(); + if (processed.approved) { + ++updated.usage; + if (!delegate()->peerListFindRow(user->id.value)) { + delegate()->peerListPrependRow( + std::make_unique(user)); + delegate()->peerListRefreshRows(); + } + } + if (updated.requested > 0) { + --updated.requested; + } + session().api().inviteLinks().applyExternalUpdate(_peer, updated); +} + +void Controller::setupAboveJoinedWidget() { using namespace Settings; auto header = object_ptr((QWidget*)nullptr); @@ -406,6 +567,8 @@ void Controller::prepare() { rpl::single(langDateTime(base::unixtime::parse(current.date)))); AddSkip(container, st::membersMarginBottom); + auto requestedWrap = addRequestedListBlock(container); + const auto listHeaderWrap = container->add( object_ptr>( container, @@ -484,6 +647,8 @@ void Controller::prepare() { } else { remaining->show(); } + + requestedWrap->toggle(data.requested > 0, anim::type::instant); }, remaining->lifetime()); rpl::combine( @@ -500,22 +665,16 @@ void Controller::prepare() { _headerWidget = header.data(); delegate()->peerListSetAboveWidget(std::move(header)); - _allLoaded = (current.usage == 0); - - const auto &inviteLinks = _peer->session().api().inviteLinks(); - const auto slice = inviteLinks.joinedFirstSliceLoaded(_peer, _link); - if (slice) { - appendSlice(*slice); - } - loadMoreRows(); } void Controller::loadMoreRows() { if (_requestId || _allLoaded) { return; } + using Flag = MTPmessages_GetChatInviteImporters::Flag; _requestId = _api.request(MTPmessages_GetChatInviteImporters( - MTP_flags(0), + MTP_flags(Flag::f_link + | (_role == Role::Requested ? Flag::f_requested : Flag(0))), _peer->input, MTP_string(_link), MTPstring(), // q @@ -536,8 +695,9 @@ void Controller::loadMoreRows() { void Controller::appendSlice(const Api::JoinedByLinkSlice &slice) { for (const auto &user : slice.users) { _lastUser = user; - delegate()->peerListAppendRow( - std::make_unique(user.user)); + delegate()->peerListAppendRow((_role == Role::Requested) + ? std::make_unique(user.user) + : std::make_unique(user.user)); } delegate()->peerListRefreshRows(); if (delegate()->peerListFullRowsCount() > 0) { @@ -552,6 +712,71 @@ void Controller::rowClicked(not_null row) { Ui::showPeerProfile(row->peer()); } +void Controller::rowActionClicked(not_null row) { + if (_role != Role::Requested) { + return; + } + delegate()->peerListShowRowMenu(row, true); +} + +base::unique_qptr Controller::rowContextMenu( + QWidget *parent, + not_null row) { + auto result = createRowContextMenu(parent, row); + + if (result) { + // First clear _menu value, so that we don't check row positions yet. + base::take(_menu); + + // Here unique_qptr is used like a shared pointer, where + // not the last destroyed pointer destroys the object, but the first. + _menu = base::unique_qptr(result.get()); + } + + return result; +} + +base::unique_qptr Controller::createRowContextMenu( + QWidget *parent, + not_null row) { + const auto user = row->peer()->asUser(); + Assert(user != nullptr); + + auto result = base::make_unique_q(parent); + const auto add = _peer->isBroadcast() + ? tr::lng_group_requests_add_channel(tr::now) + : tr::lng_group_requests_add(tr::now); + result->addAction(add, [=] { + processRequest(user, true); + }); + result->addAction(tr::lng_group_requests_dismiss(tr::now), [=] { + processRequest(user, false); + }); + return result; +} + +void Controller::processRequest( + not_null user, + bool approved) { + const auto done = crl::guard(this, [=] { + _processed.fire({ user, approved }); + if (const auto row = delegate()->peerListFindRow(user->id.value)) { + delegate()->peerListRemoveRow(row); + delegate()->peerListRefreshRows(); + } + }); + const auto fail = crl::guard(this, [=] { + _processed.fire({ user, false }); + }); + session().api().inviteLinks().processRequest( + _peer, + _data.current().link, + user, + approved, + done, + fail); +} + Main::Session &Controller::session() const { return _peer->session(); } @@ -1055,7 +1280,11 @@ void ShowInviteLinkBox( }; Ui::show( Box( - std::make_unique(peer, link.admin, std::move(data)), + std::make_unique( + peer, + link.admin, + std::move(data), + Controller::Role::Joined), std::move(initBox)), Ui::LayerOption::KeepOther); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index fd229a8f7..f0cc75e7a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -178,6 +178,16 @@ private: tr::now, lt_count_decimal, link.usageLimit - link.usage); + } else if (link.usage > 0 && link.requested > 0) { + result += ", " + tr::lng_group_invite_requested( + tr::now, + lt_count_decimal, + link.requested); + } else if (link.requested > 0) { + result = tr::lng_group_invite_requested_full( + tr::now, + lt_count_decimal, + link.requested); } if (link.expireDate > now) { const auto left = (link.expireDate - now); diff --git a/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp b/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp index f83eaf304..e597e11d6 100644 --- a/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp +++ b/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp @@ -309,8 +309,8 @@ void CreateInviteLinkBox( bool isGroup, Fn done) { EditInviteLinkBox( - box, - InviteLinkFields{ .isGroup = isGroup }, + box, + InviteLinkFields{ .isGroup = isGroup }, std::move(done)); }