/* 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_boxes.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, rpl::producer status); void prepare() override; void loadMoreRows() override; void rowClicked(not_null row) override; Main::Session &session() const override; private: const not_null _peer; rpl::producer _status; rpl::lifetime _lifetime; }; 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, data.admin, data.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()); AddSinglePeerRow( container, data.admin, rpl::single(langDateTime(base::unixtime::parse(data.date)))); } 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; } _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, rpl::producer status) : _peer(peer) , _status(std::move(status)) { } void SingleRowController::prepare() { auto row = std::make_unique(_peer); const auto raw = row.get(); std::move( _status ) | rpl::start_with_next([=](const QString &status) { raw->setCustomStatus(status); }, _lifetime); 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 AddSinglePeerRow( not_null container, not_null peer, rpl::producer status) { const auto delegate = container->lifetime().make_state< PeerListContentDelegateSimple >(); const auto controller = container->lifetime().make_state< SingleRowController >(peer, std::move(status)); controller->setStyleOverrides(&st::peerListSingleRow); const auto content = container->add(object_ptr( container, controller)); delegate->setContent(content); controller->setDelegate(delegate); } void AddPermanentLinkBlock( not_null container, not_null peer, not_null admin, rpl::producer fromList) { struct LinkData { QString link; int usage = 0; }; const auto value = container->lifetime().make_state< rpl::variable >(); if (admin->isSelf()) { *value = peer->session().changes().peerFlagsValue( peer, Data::PeerUpdate::Flag::InviteLinks ) | rpl::map([=] { const auto &links = peer->session().api().inviteLinks().myLinks( peer).links; const auto link = links.empty() ? nullptr : &links.front(); return (link && link->permanent && !link->revoked) ? LinkData{ link->link, link->usage } : LinkData(); }); } else { *value = std::move( fromList ) | rpl::map([](const Api::InviteLink &link) { return LinkData{ link.link, link.usage }; }); } const auto weak = Ui::MakeWeak(container); const auto copyLink = crl::guard(weak, [=] { if (const auto current = value->current(); !current.link.isEmpty()) { CopyInviteLink(current.link); } }); const auto shareLink = crl::guard(weak, [=] { if (const auto current = value->current(); !current.link.isEmpty()) { ShareInviteLinkBox(peer, current.link); } }); const auto revokeLink = crl::guard(weak, [=] { const auto box = std::make_shared>(); const auto done = crl::guard(weak, [=] { const auto close = [=] { if (*box) { (*box)->closeBox(); } }; peer->session().api().inviteLinks().revokePermanent( peer, admin, value->current().link, close); }); *box = Ui::show( Box(tr::lng_group_invite_about_new(tr::now), done), Ui::LayerOption::KeepOther); }); auto link = value->value( ) | rpl::map([=](const LinkData &data) { const auto prefix = qstr("https://"); return data.link.startsWith(prefix) ? data.link.mid(prefix.size()) : data.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 }; }; value->value( ) | rpl::map([=](const LinkData &data) { return peer->session().api().inviteLinks().joinedFirstSliceValue( peer, data.link, data.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, not_null admin, 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, admin, 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, link.admin ) | 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); } }