diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 2a14bab17c..48d25273a7 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -181,6 +181,8 @@ PRIVATE boxes/peers/edit_participants_box.h boxes/peers/edit_peer_info_box.cpp boxes/peers/edit_peer_info_box.h + boxes/peers/edit_peer_invite_link.cpp + boxes/peers/edit_peer_invite_link.h boxes/peers/edit_peer_invite_links.cpp boxes/peers/edit_peer_invite_links.h boxes/peers/edit_peer_type_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1ea162bb2d..1f581be7c9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1185,6 +1185,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_invite_add" = "Create a New Link"; "lng_group_invite_add_about" = "You can generate invite links that will expire after they've been used."; "lng_group_invite_expires_at" = "This link expires {when}."; +"lng_group_invite_expired_already" = "This link has expired."; "lng_group_invite_created_by" = "Link created by"; "lng_group_invite_context_copy" = "Copy"; "lng_group_invite_context_share" = "Share"; diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index 2f3bb923e2..68307dce9a 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -455,6 +455,7 @@ sendMessageGamePlayAction#dd6a8f48 = SendMessageAction; sendMessageRecordRoundAction#88f27fbc = SendMessageAction; sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; speakingInGroupCallAction#d92c2285 = SendMessageAction; +sendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction; contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; @@ -535,7 +536,7 @@ auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery; receivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage; -chatInviteExported#6e24fc9d flags:# revoked:flags.0?true permanent:flags.5?true expired:flags.6?true link:string admin_id:int date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int = ExportedChatInvite; +chatInviteExported#6e24fc9d flags:# revoked:flags.0?true permanent:flags.5?true link:string admin_id:int date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int = ExportedChatInvite; chatInviteAlready#5a686d7c chat:Chat = ChatInvite; chatInvite#dfc2f58e flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true title:string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; @@ -1463,7 +1464,7 @@ messages.getReplies#24b581ba peer:InputPeer msg_id:int offset_id:int offset_date messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage; messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool; messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory; -messages.getExportedChatInvites#6d9cae03 flags:# revoked:flags.3?true peer:InputPeer admin_id:flags.0?InputUser offset_link:flags.2?string limit:int = messages.ExportedChatInvites; +messages.getExportedChatInvites#6a72ac6c flags:# revoked:flags.3?true peer:InputPeer admin_id:flags.0?InputUser offset_date:flags.2?int offset_link:flags.2?string limit:int = messages.ExportedChatInvites; messages.editExportedChatInvite#2e4ffbe flags:# revoked:flags.2?true peer:InputPeer link:string expire_date:flags.0?int usage_limit:flags.1?int = messages.ExportedChatInvite; messages.deleteRevokedExportedChatInvites#52041463 peer:InputPeer = Bool; messages.deleteExportedChatInvite#d464a42b peer:InputPeer link:string = Bool; diff --git a/Telegram/SourceFiles/api/api_invite_links.cpp b/Telegram/SourceFiles/api/api_invite_links.cpp index 62b493fcc1..bdc2c8edb7 100644 --- a/Telegram/SourceFiles/api/api_invite_links.cpp +++ b/Telegram/SourceFiles/api/api_invite_links.cpp @@ -41,6 +41,27 @@ void RemovePermanent(PeerInviteLinks &links) { } // namespace +JoinedByLinkSlice ParseJoinedByLinkSlice( + not_null peer, + const MTPmessages_ChatInviteImporters &slice) { + auto result = JoinedByLinkSlice(); + slice.match([&](const MTPDmessages_chatInviteImporters &data) { + auto &owner = peer->session().data(); + owner.processUsers(data.vusers()); + result.count = data.vcount().v; + result.users.reserve(data.vimporters().v.size()); + for (const auto importer : data.vimporters().v) { + importer.match([&](const MTPDchatInviteImporter &data) { + result.users.push_back({ + .user = owner.user(data.vuser_id().v), + .date = data.vdate().v, + }); + }); + } + }); + return result; +} + InviteLinks::InviteLinks(not_null api) : _api(api) { } @@ -328,6 +349,7 @@ void InviteLinks::requestLinks(not_null peer) { MTP_flags(0), peer->input, MTPInputUser(), // admin_id + MTPint(), // offset_date MTPstring(), // offset_link MTP_int(kFirstPage) )).done([=](const MTPmessages_ExportedChatInvites &result) { @@ -362,9 +384,18 @@ void InviteLinks::requestLinks(not_null peer) { _firstSliceRequests.emplace(peer, requestId); } -JoinedByLinkSlice InviteLinks::lookupJoinedFirstSlice(LinkKey key) const { +std::optional InviteLinks::lookupJoinedFirstSlice( + LinkKey key) const { const auto i = _firstJoined.find(key); - return (i != end(_firstJoined)) ? i->second : JoinedByLinkSlice(); + return (i != end(_firstJoined)) + ? std::make_optional(i->second) + : std::nullopt; +} + +std::optional InviteLinks::joinedFirstSliceLoaded( + not_null peer, + const QString &link) const { + return lookupJoinedFirstSlice({ peer, link }); } rpl::producer InviteLinks::joinedFirstSliceValue( @@ -372,7 +403,7 @@ rpl::producer InviteLinks::joinedFirstSliceValue( const QString &link, int fullCount) { const auto key = LinkKey{ peer, link }; - auto current = lookupJoinedFirstSlice(key); + auto current = lookupJoinedFirstSlice(key).value_or(JoinedByLinkSlice()); if (current.count == fullCount && (!fullCount || !current.users.empty())) { return rpl::single(current); @@ -390,7 +421,7 @@ rpl::producer InviteLinks::joinedFirstSliceValue( ) | rpl::filter( _1 == key ) | rpl::map([=] { - return lookupJoinedFirstSlice(key); + return lookupJoinedFirstSlice(key).value_or(JoinedByLinkSlice()); })); } @@ -422,7 +453,7 @@ void InviteLinks::requestJoinedFirstSlice(LinkKey key) { MTP_int(kJoinedFirstPage) )).done([=](const MTPmessages_ChatInviteImporters &result) { _firstJoinedRequests.remove(key); - _firstJoined[key] = parseSlice(key.peer, result); + _firstJoined[key] = ParseJoinedByLinkSlice(key.peer, result); _joinedFirstSliceLoaded.fire_copy(key); }).fail([=](const RPCError &error) { _firstJoinedRequests.remove(key); @@ -537,27 +568,6 @@ auto InviteLinks::parseSlice( return result; } -JoinedByLinkSlice InviteLinks::parseSlice( - not_null peer, - const MTPmessages_ChatInviteImporters &slice) const { - auto result = JoinedByLinkSlice(); - slice.match([&](const MTPDmessages_chatInviteImporters &data) { - auto &owner = peer->session().data(); - owner.processUsers(data.vusers()); - result.count = data.vcount().v; - result.users.reserve(data.vimporters().v.size()); - for (const auto importer : data.vimporters().v) { - importer.match([&](const MTPDchatInviteImporter &data) { - result.users.push_back({ - .user = owner.user(data.vuser_id().v), - .date = data.vdate().v, - }); - }); - } - }); - return result; -} - auto InviteLinks::parse( not_null peer, const MTPExportedChatInvite &invite) const -> Link { @@ -578,7 +588,8 @@ auto InviteLinks::parse( void InviteLinks::requestMoreLinks( not_null peer, - const QString &last, + TimeId lastDate, + const QString &lastLink, bool revoked, Fn done) { using Flag = MTPmessages_GetExportedChatInvites::Flag; @@ -587,7 +598,8 @@ void InviteLinks::requestMoreLinks( | (revoked ? Flag::f_revoked : Flag(0))), peer->input, MTPInputUser(), // admin_id, - MTP_string(last), + MTP_int(lastDate), + MTP_string(lastLink), MTP_int(kPerPage) )).done([=](const MTPmessages_ExportedChatInvites &result) { auto slice = parseSlice(peer, result); diff --git a/Telegram/SourceFiles/api/api_invite_links.h b/Telegram/SourceFiles/api/api_invite_links.h index 72deca7096..83ad34106c 100644 --- a/Telegram/SourceFiles/api/api_invite_links.h +++ b/Telegram/SourceFiles/api/api_invite_links.h @@ -44,6 +44,10 @@ struct InviteLinkUpdate { std::optional now; }; +[[nodiscard]] JoinedByLinkSlice ParseJoinedByLinkSlice( + not_null peer, + const MTPmessages_ChatInviteImporters &slice); + class InviteLinks final { public: explicit InviteLinks(not_null api); @@ -90,6 +94,9 @@ public: not_null peer, const QString &link, int fullCount); + [[nodiscard]] std::optional joinedFirstSliceLoaded( + not_null peer, + const QString &link) const; [[nodiscard]] rpl::producer updates( not_null peer) const; [[nodiscard]] rpl::producer<> allRevokedDestroyed( @@ -97,7 +104,8 @@ public: void requestMoreLinks( not_null peer, - const QString &last, + TimeId lastDate, + const QString &lastLink, bool revoked, Fn done); @@ -122,9 +130,6 @@ private: [[nodiscard]] Link parse( not_null peer, const MTPExportedChatInvite &invite) const; - [[nodiscard]] JoinedByLinkSlice parseSlice( - not_null peer, - const MTPmessages_ChatInviteImporters &slice) const; [[nodiscard]] Link *lookupPermanent(not_null peer); [[nodiscard]] Link *lookupPermanent(Links &links); [[nodiscard]] const Link *lookupPermanent(const Links &links) const; @@ -152,7 +157,7 @@ private: int usageLimit = 0); void requestJoinedFirstSlice(LinkKey key); - [[nodiscard]] JoinedByLinkSlice lookupJoinedFirstSlice( + [[nodiscard]] std::optional lookupJoinedFirstSlice( LinkKey key) const; const not_null _api; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp new file mode 100644 index 0000000000..72b89e6430 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -0,0 +1,590 @@ +/* +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; + } + _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); + } +} diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h new file mode 100644 index 0000000000..abd3530431 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h @@ -0,0 +1,32 @@ +/* +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 +*/ +#pragma once + +#include "ui/layers/generic_box.h" + +class PeerData; + +namespace Api { +struct InviteLink; +} // namespace Api + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +void AddPermanentLinkBlock( + not_null container, + not_null peer); + +void CopyInviteLink(const QString &link); +void ShareInviteLinkBox(not_null peer, const QString &link); +void RevokeLink(not_null peer, const QString &link); + +void ShowInviteLinkBox( + not_null peer, + const Api::InviteLink &link); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index 0815386276..91d0751eca 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -7,49 +7,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/peers/edit_peer_invite_links.h" -#include "data/data_changes.h" -#include "data/data_user.h" -#include "data/data_drafts.h" -#include "data/data_session.h" -#include "data/data_histories.h" +#include "data/data_peer.h" #include "main/main_session.h" #include "api/api_invite_links.h" #include "ui/boxes/edit_invite_link.h" -#include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" -#include "ui/wrap/padding_wrap.h" -#include "ui/abstract_button.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" -#include "ui/widgets/checkbox.h" -#include "ui/widgets/input_fields.h" -#include "ui/controls/invite_link_label.h" -#include "ui/controls/invite_link_buttons.h" -#include "ui/text/text_utilities.h" -#include "ui/toast/toast.h" -#include "history/view/history_view_group_call_tracker.h" // GenerateUs... -#include "history/history_message.h" // GetErrorTextForSending. -#include "history/history.h" #include "lang/lang_keys.h" #include "boxes/confirm_box.h" -#include "boxes/peer_list_box.h" #include "boxes/peer_list_controllers.h" +#include "boxes/peers/edit_peer_invite_link.h" #include "settings/settings_common.h" // AddDivider. #include "apiwrap.h" -#include "mainwindow.h" -#include "boxes/share_box.h" #include "base/weak_ptr.h" #include "base/unixtime.h" -#include "window/window_session_controller.h" -#include "api/api_common.h" #include "styles/style_info.h" #include "styles/style_layers.h" // st::boxDividerLabel #include "styles/style_settings.h" // st::settingsDividerLabelPadding #include -#include - namespace { constexpr auto kPreloadPages = 2; @@ -189,95 +167,15 @@ private: return result; } -void CopyLink(const QString &link) { - QGuiApplication::clipboard()->setText(link); - Ui::Toast::Show(tr::lng_group_invite_copied(tr::now)); -} - -void ShareLinkBox(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))); -} - void EditLink(not_null peer, const InviteLinkData &data) { const auto creating = data.link.isEmpty(); const auto box = std::make_shared>(); using Fields = Ui::InviteLinkFields; const auto done = [=](Fields result) { const auto finish = [=](Api::InviteLink finished) { + if (creating) { + ShowInviteLinkBox(peer, finished); + } if (*box) { (*box)->closeBox(); } @@ -511,6 +409,7 @@ private: base::unique_qptr _menu; QString _offsetLink; + TimeId _offsetDate = 0; bool _requesting = false; bool _allLoaded = false; @@ -590,6 +489,7 @@ void Controller::loadMoreRows() { }; _peer->session().api().inviteLinks().requestMoreLinks( _peer, + _offsetDate, _offsetLink, _revoked, crl::guard(this, done)); @@ -602,6 +502,7 @@ void Controller::appendSlice(const InviteLinksSlice &slice) { appendRow(link, now); } _offsetLink = link.link; + _offsetDate = link.date; } if (slice.links.size() >= slice.count) { _allLoaded = true; @@ -610,7 +511,7 @@ void Controller::appendSlice(const InviteLinksSlice &slice) { } void Controller::rowClicked(not_null row) { - // #TODO links show + ShowInviteLinkBox(_peer, static_cast(row.get())->data()); } void Controller::rowActionClicked(not_null row) { @@ -647,32 +548,16 @@ base::unique_qptr Controller::createRowContextMenu( }); } else { result->addAction(tr::lng_group_invite_context_copy(tr::now), [=] { - CopyLink(link); + CopyInviteLink(link); }); result->addAction(tr::lng_group_invite_context_share(tr::now), [=] { - ShareLinkBox(_peer, link); + ShareInviteLinkBox(_peer, link); }); result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] { EditLink(_peer, data); }); result->addAction(tr::lng_group_invite_context_revoke(tr::now), [=] { - const auto box = std::make_shared>(); - const auto revoke = crl::guard(this, [=] { - const auto done = crl::guard(this, [=](InviteLinkData 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); + RevokeLink(_peer, link); }); } return result; @@ -800,178 +685,6 @@ void Controller::rowPaintIcon( } // 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()) { - CopyLink(link->link); - } - }); - const auto shareLink = crl::guard(weak, [=] { - if (const auto link = computePermanentLink()) { - ShareLinkBox(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); - })); -} - not_null AddLinksList( not_null container, not_null peer, @@ -995,11 +708,14 @@ not_null AddLinksList( void ManageInviteLinksBox( not_null box, not_null peer) { + using namespace Settings; + box->setTitle(tr::lng_group_invite_title()); const auto container = box->verticalLayout(); + AddSubsectionTitle(container, tr::lng_create_permanent_link_title()); AddPermanentLinkBlock(container, peer); - Settings::AddDivider(container); + AddDivider(container); const auto add = AddCreateLinkButton(container); add->setClickedCallback([=] { @@ -1015,7 +731,8 @@ void ManageInviteLinksBox( container, tr::lng_group_invite_add_about(), st::boxDividerLabel), - st::settingsDividerLabelPadding))); + st::settingsDividerLabelPadding)), + style::margins(0, st::inviteLinkCreateSkip, 0, 0)); const auto divider = container->add(object_ptr>( container, object_ptr(container))); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h index 365071e3a9..09b2880c39 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.h @@ -11,14 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class PeerData; -namespace Ui { -class VerticalLayout; -} // namespace Ui - -void AddPermanentLinkBlock( - not_null container, - not_null peer); - void ManageInviteLinksBox( not_null box, not_null peer); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp index 6cdda9579a..027953ac3f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "boxes/peers/edit_participants_box.h" #include "boxes/peers/edit_peer_info_box.h" // CreateButton. +#include "boxes/peers/edit_peer_invite_link.h" #include "boxes/peers/edit_peer_invite_links.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "core/application.h" @@ -541,13 +542,8 @@ object_ptr Controller::createInviteLinkBlock() { using namespace Settings; AddSkip(container); - container->add( - object_ptr( - container, - tr::lng_create_permanent_link_title(), - st::settingsSubsectionTitle), - st::settingsSubsectionTitlePadding); + AddSubsectionTitle(container, tr::lng_create_permanent_link_title()); AddPermanentLinkBlock(container, _peer); AddSkip(container); diff --git a/Telegram/SourceFiles/history/view/history_view_send_action.cpp b/Telegram/SourceFiles/history/view/history_view_send_action.cpp index c7bd84f836..37869dd3cb 100644 --- a/Telegram/SourceFiles/history/view/history_view_send_action.cpp +++ b/Telegram/SourceFiles/history/view/history_view_send_action.cpp @@ -105,6 +105,7 @@ bool SendActionPainter::updateNeedsAnimating( _speaking.emplace_or_assign( user, now + kStatusShowClientsideSpeaking); + }, [&](const MTPDsendMessageHistoryImportAction &) { }, [&](const MTPDsendMessageCancelAction &) { Unexpected("CancelAction here."); });