Allow to accept / reject requests by link.

This commit is contained in:
John Preston 2021-10-12 22:44:35 +04:00
parent 9e05e44a14
commit b4895ef730
5 changed files with 349 additions and 19 deletions

View File

@ -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<PeerData*> peer) {
_firstSliceRequests.emplace(peer, requestId);
}
void InviteLinks::processRequest(
not_null<PeerData*> peer,
const QString &link,
not_null<UserData*> user,
bool approved,
Fn<void()> done,
Fn<void()> 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<PeerData*> 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<JoinedByLinkSlice> 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(),

View File

@ -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<PeerData*> peer);
[[nodiscard]] const Links &myLinks(not_null<PeerData*> peer) const;
void processRequest(
not_null<PeerData*> peer,
const QString &link,
not_null<UserData*> user,
bool approved,
Fn<void()> done,
Fn<void()> fail);
void applyExternalUpdate(not_null<PeerData*> peer, InviteLink updated);
[[nodiscard]] rpl::producer<JoinedByLinkSlice> joinedFirstSliceValue(
not_null<PeerData*> peer,
const QString &link,
@ -136,6 +146,10 @@ private:
return (a.peer == b.peer) && (a.link == b.link);
}
};
struct ProcessRequest {
Fn<void()> done;
Fn<void()> fail;
};
[[nodiscard]] Links parseSlice(
not_null<PeerData*> peer,
@ -199,6 +213,10 @@ private:
not_null<PeerData*>,
std::vector<Fn<void()>>> _deleteRevokedCallbacks;
base::flat_map<
std::pair<not_null<PeerData*>, not_null<UserData*>>,
ProcessRequest> _processRequests;
rpl::event_stream<Update> _updates;
struct AllRevokedDestroyed {

View File

@ -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<PeerData*> 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<PeerData*> 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<PeerData*> peer,
not_null<UserData*> admin,
rpl::producer<LinkData> data);
rpl::producer<LinkData> data,
Role role);
void prepare() override;
void loadMoreRows() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowActionClicked(not_null<PeerListRow*> row) override;
Main::Session &session() const override;
rpl::producer<int> boxHeightValue() const override;
int descriptionTopSkipMin() const override;
struct Processed {
not_null<UserData*> user;
bool approved = false;
};
[[nodiscard]] rpl::producer<Processed> processed() const {
return _processed.events();
}
private:
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
void setupAboveJoinedWidget();
void appendSlice(const Api::JoinedByLinkSlice &slice);
void addHeaderBlock(not_null<Ui::VerticalLayout*> container);
not_null<Ui::SlideWrap<>*> addRequestedListBlock(
not_null<Ui::VerticalLayout*> container);
void updateWithProcessed(Processed processed);
[[nodiscard]] rpl::producer<LinkData> dataValue() const;
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row);
void processRequest(not_null<UserData*> user, bool approved);
const not_null<PeerData*> _peer;
const Role _role = Role::Joined;
rpl::variable<LinkData> _data;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<Processed> _processed;
QString _link;
bool _revoked = false;
@ -220,10 +299,12 @@ void QrBox(
Controller::Controller(
not_null<PeerData*> peer,
not_null<UserData*> admin,
rpl::producer<LinkData> data)
rpl::producer<LinkData> 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<Ui::VerticalLayout*> container) {
}, lifetime());
}
not_null<Ui::SlideWrap<>*> Controller::addRequestedListBlock(
not_null<Ui::VerticalLayout*> container) {
using namespace Settings;
auto result = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(
container)));
const auto wrap = result->entity();
// Make this container occupy full width.
wrap->add(object_ptr<Ui::RpWidget>(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<PeerListContent>(
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<PeerListRow>(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<Ui::VerticalLayout>((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<Ui::SlideWrap<Ui::VerticalLayout>>(
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<PeerListRow>(user.user));
delegate()->peerListAppendRow((_role == Role::Requested)
? std::make_unique<RequestedRow>(user.user)
: std::make_unique<PeerListRow>(user.user));
}
delegate()->peerListRefreshRows();
if (delegate()->peerListFullRowsCount() > 0) {
@ -552,6 +712,71 @@ void Controller::rowClicked(not_null<PeerListRow*> row) {
Ui::showPeerProfile(row->peer());
}
void Controller::rowActionClicked(not_null<PeerListRow*> row) {
if (_role != Role::Requested) {
return;
}
delegate()->peerListShowRowMenu(row, true);
}
base::unique_qptr<Ui::PopupMenu> Controller::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> 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<Ui::PopupMenu>(result.get());
}
return result;
}
base::unique_qptr<Ui::PopupMenu> Controller::createRowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
const auto user = row->peer()->asUser();
Assert(user != nullptr);
auto result = base::make_unique_q<Ui::PopupMenu>(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<UserData*> 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<PeerListBox>(
std::make_unique<Controller>(peer, link.admin, std::move(data)),
std::make_unique<Controller>(
peer,
link.admin,
std::move(data),
Controller::Role::Joined),
std::move(initBox)),
Ui::LayerOption::KeepOther);
}

View File

@ -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);

View File

@ -309,8 +309,8 @@ void CreateInviteLinkBox(
bool isGroup,
Fn<void(InviteLinkFields)> done) {
EditInviteLinkBox(
box,
InviteLinkFields{ .isGroup = isGroup },
box,
InviteLinkFields{ .isGroup = isGroup },
std::move(done));
}