/* 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_invite_link.h" #include "data/data_peer.h" #include "data/data_user.h" #include "data/data_changes.h" #include "data/data_session.h" #include "data/data_histories.h" #include "main/main_session.h" #include "api/api_invite_links.h" #include "base/unixtime.h" #include "apiwrap.h" #include "ui/controls/invite_link_buttons.h" #include "ui/controls/invite_link_label.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/padding_wrap.h" #include "ui/widgets/popup_menu.h" #include "ui/abstract_button.h" #include "ui/toast/toast.h" #include "ui/text/text_utilities.h" #include "boxes/share_box.h" #include "history/view/history_view_group_call_tracker.h" // GenerateUs... #include "history/history_message.h" // GetErrorTextForSending. #include "history/history.h" #include "boxes/confirm_box.h" #include "boxes/peer_list_box.h" #include "mainwindow.h" #include "facades.h" // Ui::showPerProfile. #include "lang/lang_keys.h" #include "window/window_session_controller.h" #include "settings/settings_common.h" #include "mtproto/sender.h" #include "styles/style_info.h" #include namespace { constexpr auto kFirstPage = 20; constexpr auto kPerPage = 100; using LinkData = Api::InviteLink; class Controller final : public PeerListController { public: Controller(not_null peer, const LinkData &data); void prepare() override; void loadMoreRows() override; void rowClicked(not_null row) override; Main::Session &session() const override; private: void appendSlice(const Api::JoinedByLinkSlice &slice); [[nodiscard]] object_ptr prepareHeader(); const not_null _peer; LinkData _data; mtpRequestId _requestId = 0; std::optional _lastUser; bool _allLoaded = false; MTP::Sender _api; rpl::lifetime _lifetime; }; class SingleRowController final : public PeerListController { public: SingleRowController(not_null peer, TimeId date); void prepare() override; void loadMoreRows() override; void rowClicked(not_null row) override; Main::Session &session() const override; private: const not_null _peer; TimeId _date = 0; }; void AddHeaderBlock( not_null container, not_null peer, const LinkData &data, TimeId now) { const auto link = data.link; const auto weak = Ui::MakeWeak(container); const auto copyLink = crl::guard(weak, [=] { CopyInviteLink(link); }); const auto shareLink = crl::guard(weak, [=] { ShareInviteLinkBox(peer, link); }); const auto revokeLink = crl::guard(weak, [=] { RevokeLink(peer, link); }); const auto createMenu = [=] { auto result = base::make_unique_q(container); result->addAction( tr::lng_group_invite_context_copy(tr::now), copyLink); result->addAction( tr::lng_group_invite_context_share(tr::now), shareLink); result->addAction( tr::lng_group_invite_context_revoke(tr::now), revokeLink); return result; }; const auto prefix = qstr("https://"); const auto label = container->lifetime().make_state( container, rpl::single(link.startsWith(prefix) ? link.mid(prefix.size()) : link), createMenu); container->add( label->take(), st::inviteLinkFieldPadding); label->clicks( ) | rpl::start_with_next(copyLink, label->lifetime()); if ((data.expireDate <= 0 || now < data.expireDate) && (data.usageLimit <= 0 || data.usage < data.usageLimit)) { AddCopyShareLinkButtons( container, copyLink, shareLink); } } void AddHeader( not_null container, not_null peer, const LinkData &data) { using namespace Settings; if (!data.revoked && !data.permanent) { const auto now = base::unixtime::now(); AddHeaderBlock(container, peer, data, now); AddSkip(container, st::inviteLinkJoinedRowPadding.bottom() * 2); if (data.expireDate > 0) { AddDividerText( container, (data.expireDate > now ? tr::lng_group_invite_expires_at( lt_when, rpl::single(langDateTime( base::unixtime::parse(data.expireDate)))) : tr::lng_group_invite_expired_already())); } else { AddDivider(container); } } AddSkip(container); AddSubsectionTitle( container, tr::lng_group_invite_created_by()); const auto delegate = container->lifetime().make_state< PeerListContentDelegateSimple >(); const auto controller = container->lifetime().make_state< SingleRowController >(data.admin, data.date); const auto content = container->add(object_ptr( container, controller)); delegate->setContent(content); controller->setDelegate(delegate); } Controller::Controller(not_null peer, const LinkData &data) : _peer(peer) , _data(data) , _api(&_peer->session().api().instance()) { } object_ptr Controller::prepareHeader() { using namespace Settings; auto result = object_ptr((QWidget*)nullptr); const auto container = result.data(); AddHeader(container, _peer, _data); AddDivider(container); AddSkip(container); AddSubsectionTitle( container, (_data.usage ? tr::lng_group_invite_joined( lt_count, rpl::single(float64(_data.usage))) : tr::lng_group_invite_no_joined())); return result; } void Controller::prepare() { delegate()->peerListSetAboveWidget(prepareHeader()); _allLoaded = (_data.usage == 0); const auto &inviteLinks = _peer->session().api().inviteLinks(); const auto slice = inviteLinks.joinedFirstSliceLoaded(_peer, _data.link); if (slice) { appendSlice(*slice); } loadMoreRows(); } void Controller::loadMoreRows() { if (_requestId || _allLoaded) { return; } _allLoaded = true; // #TODO links //_requestId = _api.request(MTPmessages_GetChatInviteImporters( // _peer->input, // MTP_string(_data.link), // MTP_int(_lastUser ? _lastUser->date : 0), // _lastUser ? _lastUser->user->inputUser : MTP_inputUserEmpty(), // MTP_int(_lastUser ? kPerPage : kFirstPage) //)).done([=](const MTPmessages_ChatInviteImporters &result) { // _requestId = 0; // auto slice = Api::ParseJoinedByLinkSlice(_peer, result); // _allLoaded = slice.users.empty(); // appendSlice(slice); //}).fail([=](const RPCError &error) { // _requestId = 0; // _allLoaded = true; //}).send(); } void Controller::appendSlice(const Api::JoinedByLinkSlice &slice) { for (const auto &user : slice.users) { _lastUser = user; delegate()->peerListAppendRow( std::make_unique(user.user)); } delegate()->peerListRefreshRows(); } void Controller::rowClicked(not_null row) { Ui::showPeerProfile(row->peer()); } Main::Session &Controller::session() const { return _peer->session(); } SingleRowController::SingleRowController( not_null peer, TimeId date) : _peer(peer) , _date(date) { } void SingleRowController::prepare() { auto row = std::make_unique(_peer); row->setCustomStatus(langDateTime(base::unixtime::parse(_date))); delegate()->peerListAppendRow(std::move(row)); delegate()->peerListRefreshRows(); } void SingleRowController::loadMoreRows() { } void SingleRowController::rowClicked(not_null row) { Ui::showPeerProfile(row->peer()); } Main::Session &SingleRowController::session() const { return _peer->session(); } } // namespace void AddPermanentLinkBlock( not_null container, not_null peer) { const auto computePermanentLink = [=] { const auto &links = peer->session().api().inviteLinks().links( peer).links; const auto link = links.empty() ? nullptr : &links.front(); return (link && link->permanent && !link->revoked) ? link : nullptr; }; auto value = peer->session().changes().peerFlagsValue( peer, Data::PeerUpdate::Flag::InviteLinks ) | rpl::map([=] { const auto link = computePermanentLink(); return link ? std::make_tuple(link->link, link->usage) : std::make_tuple(QString(), 0); }) | rpl::distinct_until_changed( ) | rpl::start_spawning(container->lifetime()); const auto weak = Ui::MakeWeak(container); const auto copyLink = crl::guard(weak, [=] { if (const auto link = computePermanentLink()) { CopyInviteLink(link->link); } }); const auto shareLink = crl::guard(weak, [=] { if (const auto link = computePermanentLink()) { ShareInviteLinkBox(peer, link->link); } }); const auto revokeLink = crl::guard(weak, [=] { const auto box = std::make_shared>(); const auto done = crl::guard(weak, [=] { const auto close = [=](auto&&) { if (*box) { (*box)->closeBox(); } }; peer->session().api().inviteLinks().revokePermanent(peer, close); }); *box = Ui::show( Box(tr::lng_group_invite_about_new(tr::now), done), Ui::LayerOption::KeepOther); }); auto link = rpl::duplicate( value ) | rpl::map([=](QString link, int usage) { const auto prefix = qstr("https://"); return link.startsWith(prefix) ? link.mid(prefix.size()) : link; }); const auto createMenu = [=] { auto result = base::make_unique_q(container); result->addAction( tr::lng_group_invite_context_copy(tr::now), copyLink); result->addAction( tr::lng_group_invite_context_share(tr::now), shareLink); result->addAction( tr::lng_group_invite_context_revoke(tr::now), revokeLink); return result; }; const auto label = container->lifetime().make_state( container, std::move(link), createMenu); container->add( label->take(), st::inviteLinkFieldPadding); label->clicks( ) | rpl::start_with_next(copyLink, label->lifetime()); AddCopyShareLinkButtons( container, copyLink, shareLink); struct JoinedState { QImage cachedUserpics; std::vector list; int count = 0; bool allUserpicsLoaded = false; rpl::variable content; rpl::lifetime lifetime; }; const auto state = container->lifetime().make_state(); const auto push = [=] { HistoryView::GenerateUserpicsInRow( state->cachedUserpics, state->list, st::inviteLinkUserpics, 0); state->allUserpicsLoaded = ranges::all_of( state->list, [](const HistoryView::UserpicInRow &element) { return !element.peer->hasUserpic() || element.view->image(); }); state->content = Ui::JoinedCountContent{ .count = state->count, .userpics = state->cachedUserpics }; }; std::move( value ) | rpl::map([=](QString link, int usage) { return peer->session().api().inviteLinks().joinedFirstSliceValue( peer, link, usage); }) | rpl::flatten_latest( ) | rpl::start_with_next([=](const Api::JoinedByLinkSlice &slice) { auto list = std::vector(); list.reserve(slice.users.size()); for (const auto &item : slice.users) { const auto i = ranges::find( state->list, item.user, &HistoryView::UserpicInRow::peer); if (i != end(state->list)) { list.push_back(std::move(*i)); } else { list.push_back({ item.user }); } } state->count = slice.count; state->list = std::move(list); push(); }, state->lifetime); peer->session().downloaderTaskFinished( ) | rpl::filter([=] { return !state->allUserpicsLoaded; }) | rpl::start_with_next([=] { auto pushing = false; state->allUserpicsLoaded = true; for (const auto &element : state->list) { if (!element.peer->hasUserpic()) { continue; } else if (element.peer->userpicUniqueKey(element.view) != element.uniqueKey) { pushing = true; } else if (!element.view->image()) { state->allUserpicsLoaded = false; } } if (pushing) { push(); } }, state->lifetime); Ui::AddJoinedCountButton( container, state->content.value(), st::inviteLinkJoinedRowPadding )->setClickedCallback([=] { }); container->add(object_ptr>( container, object_ptr( container, st::inviteLinkJoinedRowPadding.bottom())) )->setDuration(0)->toggleOn(state->content.value( ) | rpl::map([=](const Ui::JoinedCountContent &content) { return (content.count <= 0); })); } void CopyInviteLink(const QString &link) { QGuiApplication::clipboard()->setText(link); Ui::Toast::Show(tr::lng_group_invite_copied(tr::now)); } void ShareInviteLinkBox(not_null peer, const QString &link) { const auto session = &peer->session(); const auto sending = std::make_shared(); const auto box = std::make_shared>(); auto copyCallback = [=] { QGuiApplication::clipboard()->setText(link); Ui::Toast::Show(tr::lng_group_invite_copied(tr::now)); }; auto submitCallback = [=]( std::vector> &&result, TextWithTags &&comment, Api::SendOptions options) { if (*sending || result.empty()) { return; } const auto error = [&] { for (const auto peer : result) { const auto error = GetErrorTextForSending( peer, {}, comment); if (!error.isEmpty()) { return std::make_pair(error, peer); } } return std::make_pair(QString(), result.front()); }(); if (!error.first.isEmpty()) { auto text = TextWithEntities(); if (result.size() > 1) { text.append( Ui::Text::Bold(error.second->name) ).append("\n\n"); } text.append(error.first); Ui::show( Box(text), Ui::LayerOption::KeepOther); return; } *sending = true; if (!comment.text.isEmpty()) { comment.text = link + "\n" + comment.text; const auto add = link.size() + 1; for (auto &tag : comment.tags) { tag.offset += add; } } const auto owner = &peer->owner(); auto &api = peer->session().api(); auto &histories = owner->histories(); const auto requestType = Data::Histories::RequestType::Send; for (const auto peer : result) { const auto history = owner->history(peer); auto message = ApiWrap::MessageToSend(history); message.textWithTags = comment; message.action.options = options; message.action.clearDraft = false; api.sendMessage(std::move(message)); } Ui::Toast::Show(tr::lng_share_done(tr::now)); if (*box) { (*box)->closeBox(); } }; auto filterCallback = [](PeerData *peer) { return peer->canWrite(); }; *box = Ui::show( Box( App::wnd()->sessionController(), std::move(copyCallback), std::move(submitCallback), std::move(filterCallback)), Ui::LayerOption::KeepOther); } void RevokeLink(not_null peer, const QString &link) { const auto box = std::make_shared>(); const auto revoke = [=] { const auto done = [=](const LinkData &data) { if (*box) { (*box)->closeBox(); } }; peer->session().api().inviteLinks().revoke(peer, link, done); }; *box = Ui::show( Box( tr::lng_group_invite_revoke_about(tr::now), revoke), Ui::LayerOption::KeepOther); } void ShowInviteLinkBox( not_null peer, const Api::InviteLink &link) { auto initBox = [=](not_null box) { box->setTitle((link.permanent && !link.revoked) ? tr::lng_manage_peer_link_permanent() : tr::lng_manage_peer_link_invite()); peer->session().api().inviteLinks().updates( peer ) | rpl::start_with_next([=](const Api::InviteLinkUpdate &update) { if (update.was == link.link && (!update.now || (!link.revoked && update.now->revoked))) { box->closeBox(); } }, box->lifetime()); box->addButton(tr::lng_about_done(), [=] { box->closeBox(); }); }; if (link.usage > 0) { Ui::show( Box( std::make_unique(peer, link), std::move(initBox)), Ui::LayerOption::KeepOther); } else { Ui::show(Box([=](not_null box) { initBox(box); const auto container = box->verticalLayout(); AddHeader(container, peer, link); }), Ui::LayerOption::KeepOther); } }