/* 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/edit_peer_requests_box.h" #include "ui/effects/ripple_animation.h" #include "boxes/peer_list_controllers.h" #include "boxes/peers/edit_participants_box.h" // SubscribeToMigration #include "boxes/peers/edit_peer_invite_link.h" // PrepareRequestedRowStatus #include "history/view/history_view_requests_bar.h" // kRecentRequestsLimit #include "data/data_peer.h" #include "data/data_user.h" #include "data/data_chat.h" #include "data/data_channel.h" #include "data/data_session.h" #include "base/unixtime.h" #include "main/main_session.h" #include "mtproto/sender.h" #include "ui/text/text_utilities.h" #include "ui/toasts/common_toasts.h" #include "lang/lang_keys.h" #include "window/window_session_controller.h" #include "apiwrap.h" #include "api/api_invite_links.h" #include "styles/style_boxes.h" namespace { constexpr auto kFirstPageCount = 16; constexpr auto kPerPage = 200; constexpr auto kServerSearchDelay = crl::time(1000); 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, not_null user, TimeId date); 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, not_null user, TimeId date) : PeerListRow(user) , _delegate(delegate) { setCustomStatus(PrepareRequestedRowStatus(date)); } 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)); } } // namespace class RequestsBoxController::RowHelper final : public RowDelegate { public: explicit RowHelper(bool isGroup); [[nodiscard]] QSize rowAcceptButtonSize() override; [[nodiscard]] 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); Ui::RoundRect _acceptRect; Ui::RoundRect _acceptRectOver; Ui::RoundRect _rejectRect; Ui::RoundRect _rejectRectOver; QString _acceptText; QString _rejectText; int _acceptTextWidth = 0; int _rejectTextWidth = 0; }; RequestsBoxController::RowHelper::RowHelper(bool isGroup) : _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(isGroup ? tr::lng_group_requests_add(tr::now) : tr::lng_group_requests_add_channel(tr::now)) , _rejectText(tr::lng_group_requests_dismiss(tr::now)) , _acceptTextWidth(st::requestsAcceptButton.font->width(_acceptText)) , _rejectTextWidth(st::requestsRejectButton.font->width(_rejectText)) { } RequestsBoxController::RequestsBoxController( not_null navigation, not_null peer) : PeerListController(CreateSearchController(peer)) , _navigation(navigation) , _helper(std::make_unique(!peer->isBroadcast())) , _peer(peer) , _api(&_peer->session().mtp()) { setStyleOverrides(&st::requestsBoxList); subscribeToMigration(); } RequestsBoxController::~RequestsBoxController() = default; void RequestsBoxController::Start( not_null navigation, not_null peer) { auto controller = std::make_unique( navigation, peer->migrateToOrMe()); const auto initBox = [=](not_null box) { box->addButton(tr::lng_close(), [=] { box->closeBox(); }); }; navigation->parentController()->show( Box(std::move(controller), initBox)); } Main::Session &RequestsBoxController::session() const { return _peer->session(); } auto RequestsBoxController::CreateSearchController(not_null peer) -> std::unique_ptr { return std::make_unique(peer); } std::unique_ptr RequestsBoxController::createSearchRow( not_null peer) { if (const auto user = peer->asUser()) { return createRow(user); } return nullptr; } void RequestsBoxController::prepare() { delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled); delegate()->peerListSetTitle(_peer->isBroadcast() ? tr::lng_manage_peer_requests_channel() : tr::lng_manage_peer_requests()); setDescriptionText(tr::lng_contacts_loading(tr::now)); setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now)); loadMoreRows(); } void RequestsBoxController::loadMoreRows() { if (searchController() && searchController()->loadMoreRows()) { return; } else if (_loadRequestId || _allLoaded) { return; } // First query is small and fast, next loads a lot of rows. const auto limit = _offsetDate ? kPerPage : kFirstPageCount; using Flag = MTPmessages_GetChatInviteImporters::Flag; _loadRequestId = _api.request(MTPmessages_GetChatInviteImporters( MTP_flags(Flag::f_requested), _peer->input, MTPstring(), // link MTPstring(), // q MTP_int(_offsetDate), _offsetUser ? _offsetUser->inputUser : MTP_inputUserEmpty(), MTP_int(limit) )).done([=](const MTPmessages_ChatInviteImporters &result) { const auto firstLoad = !_offsetDate; _loadRequestId = 0; result.match([&](const MTPDmessages_chatInviteImporters &data) { const auto count = data.vcount().v; const auto &importers = data.vimporters().v; auto &owner = _peer->owner(); for (const auto &importer : importers) { importer.match([&](const MTPDchatInviteImporter &data) { _offsetDate = data.vdate().v; _offsetUser = owner.user(data.vuser_id()); appendRow(_offsetUser, _offsetDate); }); } // To be sure - wait for a whole empty result list. _allLoaded = importers.isEmpty(); }); if (_allLoaded || (firstLoad && delegate()->peerListFullRowsCount() > 0)) { refreshDescription(); } delegate()->peerListRefreshRows(); }).fail([=](const MTP::Error &error) { _loadRequestId = 0; _allLoaded = true; }).send(); } void RequestsBoxController::refreshDescription() { setDescriptionText((delegate()->peerListFullRowsCount() > 0) ? QString() : _peer->isBroadcast() ? tr::lng_group_requests_none_channel(tr::now) : tr::lng_group_requests_none(tr::now)); } void RequestsBoxController::rowClicked(not_null row) { _navigation->showPeerInfo(row->peer()); } void RequestsBoxController::rowElementClicked( not_null row, int element) { processRequest(row->peer()->asUser(), (element == kAcceptButton)); } void RequestsBoxController::processRequest( not_null user, bool approved) { const auto done = crl::guard(this, [=] { if (const auto row = delegate()->peerListFindRow(user->id.value)) { delegate()->peerListRemoveRow(row); refreshDescription(); delegate()->peerListRefreshRows(); } if (approved) { Ui::ShowMultilineToast({ .text = (_peer->isBroadcast() ? tr::lng_group_requests_was_added_channel : tr::lng_group_requests_was_added)( tr::now, lt_user, Ui::Text::Bold(user->name), Ui::Text::WithEntities) }); } pushRecentRequests(); }); const auto fail = [] {}; session().api().inviteLinks().processRequest( _peer, QString(), // link user, approved, done, fail); } void RequestsBoxController::pushRecentRequests() { const auto count = std::min( delegate()->peerListFullRowsCount(), HistoryView::kRecentRequestsLimit); if (!count) { return; } auto requests = std::vector>(); requests.reserve(count); for (auto i = 0; i != count; ++i) { requests.push_back(delegate()->peerListRowAt(i)->peer()->asUser()); } session().api().inviteLinks().pushRecentRequests({ .peer = _peer, .users = std::move(requests), }); } void RequestsBoxController::appendRow( not_null user, TimeId date) { if (!delegate()->peerListFindRow(user->id.value)) { if (auto row = createRow(user, date)) { delegate()->peerListAppendRow(std::move(row)); setDescriptionText(QString()); } } } QSize RequestsBoxController::RowHelper::rowAcceptButtonSize() { const auto &st = st::requestsAcceptButton; return { (st.width <= 0) ? (_acceptTextWidth - st.width) : st.width, st.height, }; } QSize RequestsBoxController::RowHelper::rowRejectButtonSize() { const auto &st = st::requestsRejectButton; return { (st.width <= 0) ? (_rejectTextWidth - st.width) : st.width, st.height, }; } void RequestsBoxController::RowHelper::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 RequestsBoxController::RowHelper::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 RequestsBoxController::RowHelper::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; } } const auto textLeft = geometry.x() + ((geometry.width() - textWidth) / 2); const auto textTop = geometry.y() + st.textTop; p.setFont(st.font); p.setPen(over ? st.textFgOver : st.textFg); p.drawTextLeft(textLeft, textTop, outerWidth, text); } std::unique_ptr RequestsBoxController::createRow( not_null user, TimeId date) { if (!date) { const auto search = static_cast( searchController()); date = search->dateForUser(user); } return std::make_unique(_helper.get(), user, date); } void RequestsBoxController::subscribeToMigration() { const auto chat = _peer->asChat(); if (!chat) { return; } SubscribeToMigration( chat, lifetime(), [=](not_null channel) { migrate(chat, channel); }); } void RequestsBoxController::migrate( not_null chat, not_null channel) { _peer = channel; } RequestsBoxSearchController::RequestsBoxSearchController( not_null peer) : _peer(peer) , _api(&_peer->session().mtp()) { _timer.setCallback([=] { searchOnServer(); }); } void RequestsBoxSearchController::searchQuery(const QString &query) { if (_query != query) { _query = query; _offsetDate = 0; _offsetUser = nullptr; _requestId = 0; _allLoaded = false; if (!_query.isEmpty() && !searchInCache()) { _timer.callOnce(kServerSearchDelay); } else { _timer.cancel(); } } } void RequestsBoxSearchController::searchOnServer() { Expects(!_query.isEmpty()); loadMoreRows(); } bool RequestsBoxSearchController::isLoading() { return _timer.isActive() || _requestId; } void RequestsBoxSearchController::removeFromCache(not_null user) { for (auto &entry : _cache) { auto &items = entry.second.items; const auto j = ranges::remove(items, user, &Item::user); if (j != end(items)) { entry.second.requestedCount -= (end(items) - j); items.erase(j, end(items)); } } } TimeId RequestsBoxSearchController::dateForUser(not_null user) { if (const auto i = _dates.find(user); i != end(_dates)) { return i->second; } return {}; } bool RequestsBoxSearchController::searchInCache() { const auto i = _cache.find(_query); if (i != _cache.cend()) { _requestId = 0; searchDone( _requestId, i->second.items, i->second.requestedCount); return true; } return false; } bool RequestsBoxSearchController::loadMoreRows() { if (_query.isEmpty()) { return false; } else if (_allLoaded || isLoading()) { return true; } // For search we request a lot of rows from the first query. // (because we've waited for search request by timer already, // so we don't expect it to be fast, but we want to fill cache). const auto limit = kPerPage; using Flag = MTPmessages_GetChatInviteImporters::Flag; _requestId = _api.request(MTPmessages_GetChatInviteImporters( MTP_flags(Flag::f_requested | Flag::f_q), _peer->input, MTPstring(), // link MTP_string(_query), MTP_int(_offsetDate), _offsetUser ? _offsetUser->inputUser : MTP_inputUserEmpty(), MTP_int(limit) )).done([=]( const MTPmessages_ChatInviteImporters &result, mtpRequestId requestId) { auto items = std::vector(); result.match([&](const MTPDmessages_chatInviteImporters &data) { const auto count = data.vcount().v; const auto &importers = data.vimporters().v; auto &owner = _peer->owner(); items.reserve(importers.size()); for (const auto &importer : importers) { importer.match([&](const MTPDchatInviteImporter &data) { items.push_back({ owner.user(data.vuser_id()), data.vdate().v, }); }); } }); searchDone(requestId, items, limit); auto it = _queries.find(requestId); if (it != _queries.cend()) { const auto &query = it->second.text; if (it->second.offsetDate == 0) { auto &entry = _cache[query]; entry.items = std::move(items); entry.requestedCount = limit; } _queries.erase(it); } }).fail([=](const MTP::Error &error, mtpRequestId requestId) { if (_requestId == requestId) { _requestId = 0; _allLoaded = true; delegate()->peerListSearchRefreshRows(); } }).send(); auto entry = Query(); entry.text = _query; entry.offsetDate = _offsetDate; _queries.emplace(_requestId, entry); return true; } void RequestsBoxSearchController::searchDone( mtpRequestId requestId, const std::vector &items, int requestedCount) { if (_requestId != requestId) { return; } _requestId = 0; if (!_offsetDate) { _dates.clear(); } for (const auto &[user, date] : items) { _offsetDate = date; _offsetUser = user; _dates.emplace(user, date); delegate()->peerListSearchAddRow(user); } if (items.size() < requestedCount) { // We want cache to have full information about a query with // small results count (that we don't need the second request). // So we don't wait for empty list unlike the non-search case. _allLoaded = true; } delegate()->peerListSearchRefreshRows(); }