diff --git a/Telegram/Resources/icons/info/edit/links_link.png b/Telegram/Resources/icons/info/edit/links_link.png new file mode 100644 index 000000000..334eab0a2 Binary files /dev/null and b/Telegram/Resources/icons/info/edit/links_link.png differ diff --git a/Telegram/Resources/icons/info/edit/links_link@2x.png b/Telegram/Resources/icons/info/edit/links_link@2x.png new file mode 100644 index 000000000..6c1174a8a Binary files /dev/null and b/Telegram/Resources/icons/info/edit/links_link@2x.png differ diff --git a/Telegram/Resources/icons/info/edit/links_link@3x.png b/Telegram/Resources/icons/info/edit/links_link@3x.png new file mode 100644 index 000000000..f4d797b7f Binary files /dev/null and b/Telegram/Resources/icons/info/edit/links_link@3x.png differ diff --git a/Telegram/Resources/icons/info/edit/roundbtn_plus.png b/Telegram/Resources/icons/info/edit/roundbtn_plus.png new file mode 100644 index 000000000..86138484e Binary files /dev/null and b/Telegram/Resources/icons/info/edit/roundbtn_plus.png differ diff --git a/Telegram/Resources/icons/info/edit/roundbtn_plus@2x.png b/Telegram/Resources/icons/info/edit/roundbtn_plus@2x.png new file mode 100644 index 000000000..f2772af06 Binary files /dev/null and b/Telegram/Resources/icons/info/edit/roundbtn_plus@2x.png differ diff --git a/Telegram/Resources/icons/info/edit/roundbtn_plus@3x.png b/Telegram/Resources/icons/info/edit/roundbtn_plus@3x.png new file mode 100644 index 000000000..30bd16f13 Binary files /dev/null and b/Telegram/Resources/icons/info/edit/roundbtn_plus@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 1eb58fc27..37509fd5d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1203,7 +1203,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_invite_usage_about" = "You can make the link expire after it has been used for a certain number of times."; "lng_group_invite_usage_custom" = "Enter custom limit"; - "lng_channel_public_link_copied" = "Link copied to clipboard."; "lng_context_about_private_link" = "This link will only work for members of this chat."; diff --git a/Telegram/SourceFiles/api/api_invite_links.cpp b/Telegram/SourceFiles/api/api_invite_links.cpp index 3000fb7ef..83d4c4838 100644 --- a/Telegram/SourceFiles/api/api_invite_links.cpp +++ b/Telegram/SourceFiles/api/api_invite_links.cpp @@ -20,6 +20,7 @@ namespace { constexpr auto kFirstPage = 10; constexpr auto kPerPage = 50; +constexpr auto kJoinedFirstPage = 10; void BringPermanentToFront(PeerInviteLinks &links) { auto &list = links.links; @@ -145,7 +146,7 @@ void InviteLinks::performEdit( bool revoke, TimeId expireDate, int usageLimit) { - const auto key = EditKey{ peer, link }; + const auto key = LinkKey{ peer, link }; if (const auto i = _editCallbacks.find(key); i != end(_editCallbacks)) { if (done) { i->second.push_back(std::move(done)); @@ -250,6 +251,58 @@ void InviteLinks::requestLinks(not_null peer) { _firstSliceRequests.emplace(peer, requestId); } +JoinedByLinkSlice InviteLinks::lookupJoinedFirstSlice(LinkKey key) const { + const auto i = _firstJoined.find(key); + return (i != end(_firstJoined)) ? i->second : JoinedByLinkSlice(); +} + +rpl::producer InviteLinks::joinedFirstSliceValue( + not_null peer, + const QString &link, + int fullCount) { + const auto key = LinkKey{ peer, link }; + auto current = lookupJoinedFirstSlice(key); + if (current.count == fullCount + && (!fullCount || !current.users.empty())) { + return rpl::single(current); + } + current.count = fullCount; + const auto remove = int(current.users.size()) - current.count; + if (remove > 0) { + current.users.erase(end(current.users) - remove, end(current.users)); + } + requestJoinedFirstSlice(key); + using namespace rpl::mappers; + return rpl::single( + current + ) | rpl::then(_joinedFirstSliceLoaded.events( + ) | rpl::filter( + _1 == key + ) | rpl::map([=] { + return lookupJoinedFirstSlice(key); + })); +} + +void InviteLinks::requestJoinedFirstSlice(LinkKey key) { + if (_firstJoinedRequests.contains(key)) { + return; + } + const auto requestId = _api->request(MTPmessages_GetChatInviteImporters( + key.peer->input, + MTP_string(key.link), + MTP_int(0), // offset_date + MTP_inputUserEmpty(), // offset_user + MTP_int(kJoinedFirstPage) + )).done([=](const MTPmessages_ChatInviteImporters &result) { + _firstJoinedRequests.remove(key); + _firstJoined[key] = parseSlice(key.peer, result); + _joinedFirstSliceLoaded.fire_copy(key); + }).fail([=](const RPCError &error) { + _firstJoinedRequests.remove(key); + }).send(); + _firstJoinedRequests.emplace(key, requestId); +} + void InviteLinks::setPermanent( not_null peer, const MTPExportedChatInvite &invite) { @@ -320,6 +373,27 @@ 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 { diff --git a/Telegram/SourceFiles/api/api_invite_links.h b/Telegram/SourceFiles/api/api_invite_links.h index 6c53c9fbb..8d78a8f18 100644 --- a/Telegram/SourceFiles/api/api_invite_links.h +++ b/Telegram/SourceFiles/api/api_invite_links.h @@ -28,6 +28,16 @@ struct PeerInviteLinks { int count = 0; }; +struct JoinedByLinkUser { + not_null user; + TimeId date = 0; +}; + +struct JoinedByLinkSlice { + std::vector users; + int count = 0; +}; + class InviteLinks final { public: explicit InviteLinks(not_null api); @@ -59,21 +69,29 @@ public: void requestLinks(not_null peer); [[nodiscard]] const Links &links(not_null peer) const; + [[nodiscard]] rpl::producer joinedFirstSliceValue( + not_null peer, + const QString &link, + int fullCount); + void requestMoreLinks( not_null peer, const QString &last, Fn done); private: - struct EditKey { + struct LinkKey { not_null peer; QString link; - friend inline bool operator<(const EditKey &a, const EditKey &b) { + friend inline bool operator<(const LinkKey &a, const LinkKey &b) { return (a.peer == b.peer) ? (a.link < b.link) : (a.peer < b.peer); } + friend inline bool operator==(const LinkKey &a, const LinkKey &b) { + return (a.peer == b.peer) && (a.link == b.link); + } }; [[nodiscard]] Links parseSlice( @@ -82,6 +100,9 @@ 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; @@ -108,15 +129,23 @@ private: TimeId expireDate = 0, int usageLimit = 0); + void requestJoinedFirstSlice(LinkKey key); + [[nodiscard]] JoinedByLinkSlice lookupJoinedFirstSlice( + LinkKey key) const; + const not_null _api; base::flat_map, Links> _firstSlices; base::flat_map, mtpRequestId> _firstSliceRequests; + base::flat_map _firstJoined; + base::flat_map _firstJoinedRequests; + rpl::event_stream _joinedFirstSliceLoaded; + base::flat_map< not_null, std::vector>> _createCallbacks; - base::flat_map>> _editCallbacks; + base::flat_map>> _editCallbacks; }; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index a7e2fad86..482a9b91b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -8,15 +8,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_invite_links.h" #include "data/data_changes.h" -#include "data/data_peer.h" +#include "data/data_user.h" #include "main/main_session.h" #include "api/api_invite_links.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/padding_wrap.h" +#include "ui/abstract_button.h" #include "ui/widgets/popup_menu.h" #include "ui/controls/invite_link_label.h" #include "ui/controls/invite_link_buttons.h" #include "ui/toast/toast.h" +#include "history/view/history_view_group_call_tracker.h" // GenerateUs... #include "lang/lang_keys.h" #include "apiwrap.h" #include "styles/style_info.h" @@ -40,7 +42,8 @@ void AddPermanentLinkBlock( return link ? std::make_tuple(link->link, link->usage) : std::make_tuple(QString(), 0); - }) | rpl::start_spawning(container->lifetime()); + }) | rpl::distinct_until_changed( + ) | rpl::start_spawning(container->lifetime()); const auto copyLink = [=] { if (const auto link = computePermanentLink()) { @@ -95,4 +98,84 @@ void AddPermanentLinkBlock( 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([=] { + }); } diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 534caccde..fd9997ef6 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -9,6 +9,7 @@ using "ui/basic.style"; using "boxes/boxes.style"; using "ui/widgets/widgets.style"; +using "ui/chat/chat.style"; // GroupCallUserpics. InfoToggle { color: color; @@ -865,3 +866,12 @@ inviteLinkShare: RoundButton(inviteLinkCopy) { icon: icon {{ "info/edit/links_share", activeButtonFg }}; iconOver: icon {{ "info/edit/links_share", activeButtonFgOver }}; } +inviteLinkUserpics: GroupCallUserpics { + size: 28px; + shift: 6px; + stroke: 2px; + align: align(left); +} +inviteLinkUserpicsSkip: 8px; +inviteLinkJoinedFont: font(14px); +inviteLinkJoinedRowPadding: margins(0px, 18px, 0px, 0px); diff --git a/Telegram/SourceFiles/ui/controls/invite_link_buttons.cpp b/Telegram/SourceFiles/ui/controls/invite_link_buttons.cpp index 233ee0f51..8b8fc3cd9 100644 --- a/Telegram/SourceFiles/ui/controls/invite_link_buttons.cpp +++ b/Telegram/SourceFiles/ui/controls/invite_link_buttons.cpp @@ -11,9 +11,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "ui/wrap/padding_wrap.h" #include "lang/lang_keys.h" +#include "styles/style_chat.h" #include "styles/style_info.h" namespace Ui { +namespace { + +class JoinedCountButton final : public AbstractButton { +public: + using AbstractButton::AbstractButton; + + void onStateChanged(State was, StateChangeSource source) override { + update(); + } +}; + +} // namespace void AddCopyShareLinkButtons( not_null container, @@ -47,4 +60,78 @@ void AddCopyShareLinkButtons( }, wrap->lifetime()); } +not_null AddJoinedCountButton( + not_null container, + rpl::producer content, + style::margins padding) { + struct State { + JoinedCountContent content; + QString phrase; + int addedWidth = 0; + }; + const auto wrap = container->add( + object_ptr( + container, + st::inviteLinkUserpics.size), + padding); + const auto result = CreateChild(wrap); + const auto state = result->lifetime().make_state(); + std::move( + content + ) | rpl::start_with_next([=](JoinedCountContent &&content) { + state->content = std::move(content); + result->setAttribute( + Qt::WA_TransparentForMouseEvents, + !state->content.count); + const auto &st = st::inviteLinkUserpics; + const auto imageWidth = !state->content.userpics.isNull() + ? state->content.userpics.width() / style::DevicePixelRatio() + : !state->content.count + ? 0 + : ((std::min(state->content.count, 3) - 1) * (st.size - st.shift) + + st.size); + state->addedWidth = imageWidth + ? (imageWidth + st::inviteLinkUserpicsSkip) + : 0; + state->phrase = state->content.count + ? tr::lng_group_invite_joined( + tr::now, + lt_count_decimal, + state->content.count) + : tr::lng_group_invite_no_joined(tr::now); + const auto fullWidth = st::inviteLinkJoinedFont->width(state->phrase) + + state->addedWidth; + result->resize(fullWidth, st.size); + result->move((wrap->width() - fullWidth) / 2, 0); + result->update(); + }, result->lifetime()); + + result->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(result); + if (!state->content.userpics.isNull()) { + p.drawImage(0, 0, state->content.userpics); + } + const auto &font = st::inviteLinkJoinedFont; + p.setPen(state->content.count + ? st::defaultLinkButton.color + : st::windowSubTextFg); + p.setFont((result->isOver() || result->isDown()) + ? font->underline() + : font); + const auto top = (result->height() - font->height) / 2; + p.drawText( + state->addedWidth, + top + font->ascent, + state->phrase); + }, result->lifetime()); + + wrap->widthValue( + ) | rpl::start_with_next([=](int width) { + result->move((width - result->width()) / 2, 0); + }, wrap->lifetime()); + + return result; +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/invite_link_buttons.h b/Telegram/SourceFiles/ui/controls/invite_link_buttons.h index aa637dbb9..c21195b76 100644 --- a/Telegram/SourceFiles/ui/controls/invite_link_buttons.h +++ b/Telegram/SourceFiles/ui/controls/invite_link_buttons.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { +class AbstractButton; class VerticalLayout; void AddCopyShareLinkButtons( @@ -16,4 +17,14 @@ void AddCopyShareLinkButtons( Fn copyLink, Fn shareLink); +struct JoinedCountContent { + int count = 0; + QImage userpics; +}; + +not_null AddJoinedCountButton( + not_null container, + rpl::producer content, + style::margins padding); + } // namespace Ui