/* 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/filters/edit_filter_links.h" #include "apiwrap.h" #include "boxes/peers/edit_peer_invite_link.h" // InviteLinkQrBox. #include "boxes/peer_list_box.h" #include "boxes/premium_limits_box.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_chat_filters.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/history.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "main/main_session.h" #include "settings/settings_common.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/invite_link_buttons.h" #include "ui/controls/invite_link_label.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/popup_menu.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/painter.h" #include "window/window_session_controller.h" #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_settings.h" namespace { constexpr auto kMaxLinkTitleLength = 32; using InviteLinkData = Data::ChatFilterLink; class LinkRow; enum class Color { Permanent, Count, }; struct InviteLinkAction { enum class Type { Copy, Share, Edit, Delete, }; QString link; Type type = Type::Copy; }; struct Errors { QString status; QString toast; }; [[nodiscard]] std::optional ErrorForSharing( not_null history) { const auto result = [](const QString &status, const QString &toast) { return Errors{ status, toast }; }; const auto peer = history->peer; if (const auto user = peer->asUser()) { return user->isBot() ? result( tr::lng_filters_link_bot_status(tr::now), tr::lng_filters_link_bot_error(tr::now)) : result( tr::lng_filters_link_private_status(tr::now), tr::lng_filters_link_private_error(tr::now)); } else if (const auto chat = history->peer->asChat()) { if (!chat->canHaveInviteLink()) { return result( tr::lng_filters_link_noadmin_status(tr::now), tr::lng_filters_link_noadmin_group_error(tr::now)); } return std::nullopt; } else if (const auto channel = history->peer->asChannel()) { if (!channel->canHaveInviteLink() && (!channel->hasUsername() || channel->requestToJoin())) { return result( tr::lng_filters_link_noadmin_status(tr::now), (channel->isMegagroup() ? tr::lng_filters_link_noadmin_group_error(tr::now) : tr::lng_filters_link_noadmin_channel_error(tr::now))); } return std::nullopt; } Unexpected("Peer type in ErrorForSharing."); } void ShowSaveError( not_null window, QString error) { const auto session = &window->session(); if (error == u"CHATLISTS_TOO_MUCH"_q) { window->show(Box(ShareableFiltersLimitBox, session)); } else if (error == u"INVITES_TOO_MUCH"_q) { window->show(Box(FilterLinksLimitBox, session)); } else if (error == u"CHANNELS_TOO_MUCH"_q) { window->show(Box(ChannelsLimitBox, session)); } else if (error == u"USER_CHANNELS_TOO_MUCH"_q) { window->showToast( { tr::lng_filters_link_group_admin_error(tr::now) }); } else { window->showToast(error); } } void ShowEmptyLinkError(not_null window) { ShowSaveError(window, tr::lng_filters_empty(tr::now)); } void ChatFilterLinkBox( not_null box, not_null session, Data::ChatFilterLink data) { using namespace rpl::mappers; const auto link = data.url; box->setTitle(tr::lng_group_invite_edit_title()); const auto container = box->verticalLayout(); const auto labelField = container->add( object_ptr( container, st::defaultInputField, tr::lng_group_invite_label_header(), data.title), style::margins( st::settingsSubsectionTitlePadding.left(), st::settingsSectionSkip, st::settingsSubsectionTitlePadding.right(), st::settingsSectionSkip * 2)); labelField->setMaxLength(kMaxLinkTitleLength); Settings::AddDivider(container); box->setFocusCallback([=] { labelField->setFocusFast(); }); const auto &saveLabel = link.isEmpty() ? tr::lng_formatting_link_create : tr::lng_settings_save; box->addButton(saveLabel(), [=] { session->data().chatsFilters().edit( data.id, data.url, labelField->getLastText().trimmed()); box->closeBox(); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } class LinkRowDelegate { public: virtual void rowUpdateRow(not_null row) = 0; virtual void rowPaintIcon( QPainter &p, int x, int y, int size, Color color) = 0; }; class LinkRow final : public PeerListRow { public: LinkRow(not_null delegate, const InviteLinkData &data); void update(const InviteLinkData &data); [[nodiscard]] InviteLinkData data() const; QString generateName() override; QString generateShortName() override; PaintRoundImageCallback generatePaintUserpicCallback( bool forceRound) override; QSize rightActionSize() const override; QMargins rightActionMargins() const override; void rightActionPaint( Painter &p, int x, int y, int outerWidth, bool selected, bool actionSelected) override; private: const not_null _delegate; InviteLinkData _data; QString _status; Color _color = Color::Permanent; }; class ChatRow final : public PeerListRow { public: ChatRow(not_null peer, const QString &status, bool disabled); PaintRoundImageCallback generatePaintUserpicCallback( bool forceRound) override; private: const bool _disabled = false; QImage _disabledFrame; InMemoryKey _userpicKey; int _paletteVersion = 0; }; [[nodiscard]] Color ComputeColor(const InviteLinkData &link) { return Color::Permanent; } [[nodiscard]] QString ComputeStatus(const InviteLinkData &link) { return tr::lng_filters_chats_count(tr::now, lt_count, link.chats.size()); } LinkRow::LinkRow( not_null delegate, const InviteLinkData &data) : PeerListRow(UniqueRowIdFromString(data.url)) , _delegate(delegate) , _data(data) , _color(ComputeColor(data)) { setCustomStatus(ComputeStatus(data)); } void LinkRow::update(const InviteLinkData &data) { _data = data; _color = ComputeColor(data); setCustomStatus(ComputeStatus(data)); refreshName(st::inviteLinkList.item); _delegate->rowUpdateRow(this); } InviteLinkData LinkRow::data() const { return _data; } QString LinkRow::generateName() { if (!_data.title.isEmpty()) { return _data.title; } auto result = _data.url; return result.replace( u"https://"_q, QString() ).replace( u"t.me/+"_q, QString() ).replace( u"t.me/joinchat/"_q, QString() ); } QString LinkRow::generateShortName() { return generateName(); } PaintRoundImageCallback LinkRow::generatePaintUserpicCallback( bool forceRound) { return [=]( QPainter &p, int x, int y, int outerWidth, int size) { _delegate->rowPaintIcon(p, x, y, size, _color); }; } QSize LinkRow::rightActionSize() const { return QSize( st::inviteLinkThreeDotsIcon.width(), st::inviteLinkThreeDotsIcon.height()); } QMargins LinkRow::rightActionMargins() const { return QMargins( 0, (st::inviteLinkList.item.height - rightActionSize().height()) / 2, st::inviteLinkThreeDotsSkip, 0); } void LinkRow::rightActionPaint( Painter &p, int x, int y, int outerWidth, bool selected, bool actionSelected) { (actionSelected ? st::inviteLinkThreeDotsIconOver : st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth); } ChatRow::ChatRow( not_null peer, const QString &status, bool disabled) : PeerListRow(peer) , _disabled(disabled) { if (!status.isEmpty()) { setCustomStatus(status); } } PaintRoundImageCallback ChatRow::generatePaintUserpicCallback( bool forceRound) { const auto peer = this->peer(); const auto saved = peer->isSelf(); const auto replies = peer->isRepliesChat(); auto userpic = (saved || replies) ? Ui::PeerUserpicView() : ensureUserpicView(); auto paint = [=]( Painter &p, int x, int y, int outerWidth, int size) mutable { if (forceRound && peer->isForum()) { ForceRoundUserpicCallback(peer)(p, x, y, outerWidth, size); } else if (saved) { Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size); } else if (replies) { Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size); } else { peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size); } }; if (!_disabled) { return paint; } return [=]( Painter &p, int x, int y, int outerWidth, int size) mutable { const auto wide = size + style::ConvertScale(3); const auto full = QSize(wide, wide) * style::DevicePixelRatio(); auto repaint = false; if (_disabledFrame.size() != full) { repaint = true; _disabledFrame = QImage( full, QImage::Format_ARGB32_Premultiplied); _disabledFrame.setDevicePixelRatio(style::DevicePixelRatio()); } else { repaint = (_paletteVersion != style::PaletteVersion()) || (!saved && !replies && (_userpicKey != peer->userpicUniqueKey(userpic))); } if (repaint) { _paletteVersion = style::PaletteVersion(); _userpicKey = peer->userpicUniqueKey(userpic); _disabledFrame.fill(Qt::transparent); auto p = Painter(&_disabledFrame); paint(p, 0, 0, wide, size); auto hq = PainterHighQualityEnabler(p); p.setBrush(st::boxBg); p.setPen(Qt::NoPen); const auto two = style::ConvertScaleExact(2.5); const auto half = size / 2.; const auto rect = QRectF(half, half, half, half).translated( { two, two }); p.drawEllipse(rect); auto pen = st::windowSubTextFg->p; const auto width = style::ConvertScaleExact(1.5); const auto dash = 0.55; const auto dashWithCaps = dash + 1.; pen.setWidthF(width); // 11 parts = M_PI * half / ((dashWithCaps + space) * width) // 11 = M_PI * half / ((dashWithCaps + space) * width) // space = (M_PI * half / (11 * width)) - dashWithCaps const auto space = M_PI * half / (11 * width) - dashWithCaps; pen.setDashPattern(QVector{ dash, space }); pen.setDashOffset(1.); pen.setCapStyle(Qt::RoundCap); p.setBrush(Qt::NoBrush); p.setPen(pen); p.drawEllipse(rect.marginsRemoved({ two, two, two, two })); } p.drawImage(x, y, _disabledFrame); }; } class LinksController final : public PeerListController , public LinkRowDelegate , public base::has_weak_ptr { public: LinksController( not_null window, rpl::producer> content, Fn currentFilter); void prepare() override; void rowClicked(not_null row) override; void rowRightActionClicked(not_null row) override; base::unique_qptr rowContextMenu( QWidget *parent, not_null row) override; Main::Session &session() const override; void rowUpdateRow(not_null row) override; void rowPaintIcon( QPainter &p, int x, int y, int size, Color color) override; private: void appendRow(const InviteLinkData &data); void rebuild(const std::vector &rows); [[nodiscard]] base::unique_qptr createRowContextMenu( QWidget *parent, not_null row); const not_null _window; Fn _currentFilter; rpl::variable> _rows; base::unique_qptr _menu; std::array _icons; rpl::lifetime _lifetime; }; class LinkController final : public PeerListController , public base::has_weak_ptr { public: LinkController( not_null window, const Data::ChatFilter &filter, InviteLinkData data); void prepare() override; void rowClicked(not_null row) override; Main::Session &session() const override; void showFinished() override; [[nodiscard]] rpl::producer hasChangesValue() const; [[nodiscard]] base::flat_set> selected() const; private: void setupAboveWidget(); void setupBelowWidget(); void addHeader(not_null container); void addLinkBlock(not_null container); void toggleAllSelected(bool select); const not_null _window; InviteLinkData _data; QString _filterTitle; base::flat_set> _filterChats; base::flat_map, QString> _denied; rpl::variable>> _selected; base::flat_set> _initial; base::unique_qptr _menu; QString _link; rpl::variable _hasChanges = false; rpl::event_stream<> _showFinished; rpl::lifetime _lifetime; }; LinkController::LinkController( not_null window, const Data::ChatFilter &filter, InviteLinkData data) : _window(window) , _filterTitle(filter.title()) , _filterChats(filter.always()) { _data = std::move(data); _link = _data.url; } void LinkController::addHeader(not_null container) { using namespace Settings; const auto divider = Ui::CreateChild( container.get()); const auto verticalLayout = container->add( object_ptr(container.get())); auto icon = CreateLottieIcon( verticalLayout, { .name = u"cloud_filters"_q, .sizeOverride = { st::settingsFilterIconSize, st::settingsFilterIconSize, }, }, st::settingsFilterIconPadding); _showFinished.events( ) | rpl::start_with_next([animate = std::move(icon.animate)] { animate(anim::repeat::once); }, verticalLayout->lifetime()); verticalLayout->add(std::move(icon.widget)); verticalLayout->add( object_ptr>( verticalLayout, object_ptr( verticalLayout, (_data.url.isEmpty() ? tr::lng_filters_link_no_about(Ui::Text::WithEntities) : tr::lng_filters_link_share_about( lt_folder, rpl::single(Ui::Text::Bold(_filterTitle)), Ui::Text::WithEntities)), st::settingsFilterDividerLabel)), st::filterLinkDividerLabelPadding); verticalLayout->geometryValue( ) | rpl::start_with_next([=](const QRect &r) { divider->setGeometry(r); }, divider->lifetime()); } object_ptr DeleteLinkBox( not_null window, const InviteLinkData &link) { const auto sure = [=](Fn &&close) { window->session().data().chatsFilters().destroy(link.id, link.url); close(); }; return Ui::MakeConfirmBox({ .text = tr::lng_filters_link_delete_sure(tr::now), .confirmed = sure, .confirmText = tr::lng_box_delete(tr::now), }); } void LinkController::addLinkBlock(not_null container) { using namespace Settings; const auto link = _data.url; const auto weak = Ui::MakeWeak(container); const auto copyLink = crl::guard(weak, [=] { CopyInviteLink(delegate()->peerListUiShow(), link); }); const auto shareLink = crl::guard(weak, [=] { delegate()->peerListUiShow()->showBox( ShareInviteLinkBox(&_window->session(), link)); }); const auto getLinkQr = crl::guard(weak, [=] { delegate()->peerListUiShow()->showBox( InviteLinkQrBox(link, tr::lng_filters_link_qr_about())); }); const auto editLink = crl::guard(weak, [=] { delegate()->peerListUiShow()->showBox( Box(ChatFilterLinkBox, &_window->session(), _data)); }); const auto deleteLink = crl::guard(weak, [=] { delegate()->peerListUiShow()->showBox(DeleteLinkBox(_window, _data)); }); const auto createMenu = [=] { auto result = base::make_unique_q( container, st::popupMenuWithIcons); result->addAction( tr::lng_group_invite_context_copy(tr::now), copyLink, &st::menuIconCopy); result->addAction( tr::lng_group_invite_context_share(tr::now), shareLink, &st::menuIconShare); result->addAction( tr::lng_group_invite_context_qr(tr::now), getLinkQr, &st::menuIconQrCode); result->addAction( tr::lng_filters_link_name_it(tr::now), editLink, &st::menuIconEdit); result->addAction( tr::lng_group_invite_context_delete(tr::now), deleteLink, &st::menuIconDelete); return result; }; AddSubsectionTitle( container, tr::lng_filters_link_subtitle(), st::filterLinkSubsectionTitlePadding); const auto prefix = u"https://"_q; 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()); AddCopyShareLinkButtons(container, copyLink, shareLink); AddSkip(container, st::inviteLinkJoinedRowPadding.bottom() * 2); AddSkip(container); AddDivider(container); } void LinkController::prepare() { Expects(!_data.url.isEmpty() || _data.chats.empty()); for (const auto &history : _data.chats) { const auto peer = history->peer; auto row = std::make_unique( peer, FilterChatStatusText(peer), false); const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); delegate()->peerListSetRowChecked(raw, true); raw->finishCheckedAnimation(); _initial.emplace(peer); } for (const auto &history : _filterChats) { if (delegate()->peerListFindRow(history->peer->id.value)) { continue; } const auto peer = history->peer; const auto error = ErrorForSharing(history); auto row = std::make_unique( peer, error ? error->status : FilterChatStatusText(peer), error.has_value()); delegate()->peerListAppendRow(std::move(row)); if (error) { _denied.emplace(peer, error->toast); } else if (_data.url.isEmpty()) { _denied.emplace(peer); } } setupAboveWidget(); setupBelowWidget(); delegate()->peerListRefreshRows(); _selected = _initial; } void LinkController::rowClicked(not_null row) { const auto peer = row->peer(); if (const auto i = _denied.find(peer); i != end(_denied)) { if (!i->second.isEmpty()) { delegate()->peerListUiShow()->showToast(i->second); } } else { const auto checked = row->checked(); auto selected = _selected.current(); delegate()->peerListSetRowChecked(row, !checked); if (checked) { selected.remove(peer); } else { selected.emplace(peer); } const auto has = (_initial != selected); _selected = std::move(selected); _hasChanges = has; } } void LinkController::toggleAllSelected(bool select) { auto selected = _selected.current(); if (!select) { if (selected.empty()) { return; } for (const auto &peer : selected) { const auto row = delegate()->peerListFindRow(peer->id.value); Assert(row != nullptr); delegate()->peerListSetRowChecked(row, false); } selected = {}; } else { const auto count = delegate()->peerListFullRowsCount(); for (auto i = 0; i != count; ++i) { const auto row = delegate()->peerListRowAt(i); const auto peer = row->peer(); if (!_denied.contains(peer)) { delegate()->peerListSetRowChecked(row, true); selected.emplace(peer); } } } const auto has = (_initial != selected); _selected = std::move(selected); _hasChanges = has; } void LinkController::showFinished() { _showFinished.fire({}); } void LinkController::setupAboveWidget() { using namespace Settings; auto wrap = object_ptr((QWidget*)nullptr); const auto container = wrap.data(); addHeader(container); if (!_data.url.isEmpty()) { addLinkBlock(container); } auto subtitle = _selected.value( ) | rpl::map([=](const base::flat_set> &selected) { return _data.url.isEmpty() ? tr::lng_filters_link_chats_no(tr::now) : selected.empty() ? tr::lng_filters_link_chats_none(tr::now) : tr::lng_filters_link_chats( tr::now, lt_count, float64(selected.size())); }); const auto mayBeSelected = delegate()->peerListFullRowsCount() - int(_denied.size()); auto selectedCount = _selected.value( ) | rpl::map([](const base::flat_set> &selected) { return int(selected.size()); }); AddFilterSubtitleWithToggles( container, std::move(subtitle), mayBeSelected, std::move(selectedCount), [=](bool select) { toggleAllSelected(select); }); // Fix label cutting on text change from smaller to longer. _selected.changes() | rpl::start_with_next([=] { container->resizeToWidth(container->widthNoMargins()); }, container->lifetime()); delegate()->peerListSetAboveWidget(std::move(wrap)); } void LinkController::setupBelowWidget() { delegate()->peerListSetBelowWidget( object_ptr( (QWidget*)nullptr, object_ptr( (QWidget*)nullptr, (_data.url.isEmpty() ? tr::lng_filters_link_chats_no_about() : tr::lng_filters_link_chats_about()), st::boxDividerLabel), st::settingsDividerLabelPadding)); } Main::Session &LinkController::session() const { return _window->session(); } rpl::producer LinkController::hasChangesValue() const { return _hasChanges.value(); } base::flat_set> LinkController::selected() const { return _selected.current(); } LinksController::LinksController( not_null window, rpl::producer> content, Fn currentFilter) : _window(window) , _currentFilter(std::move(currentFilter)) , _rows(std::move(content)) { style::PaletteChanged( ) | rpl::start_with_next([=] { for (auto &image : _icons) { image = QImage(); } }, _lifetime); } void LinksController::prepare() { _rows.value( ) | rpl::start_with_next([=](const std::vector &rows) { rebuild(rows); }, _lifetime); } void LinksController::rebuild(const std::vector &rows) { auto i = 0; auto count = delegate()->peerListFullRowsCount(); while (i < rows.size()) { if (i < count) { const auto row = delegate()->peerListRowAt(i); static_cast(row.get())->update(rows[i]); } else { appendRow(rows[i]); } ++i; } while (i < count) { delegate()->peerListRemoveRow(delegate()->peerListRowAt(i)); --count; } delegate()->peerListRefreshRows(); } void LinksController::rowClicked(not_null row) { const auto link = static_cast(row.get())->data(); delegate()->peerListUiShow()->showBox( ShowLinkBox(_window, _currentFilter(), link)); } void LinksController::rowRightActionClicked(not_null row) { delegate()->peerListShowRowMenu(row, true); } base::unique_qptr LinksController::rowContextMenu( QWidget *parent, not_null row) { auto result = createRowContextMenu(parent, row); if (result) { // First clear _menu value, so that we don't check row positions yet. base::take(_menu); // Here unique_qptr is used like a shared pointer, where // not the last destroyed pointer destroys the object, but the first. _menu = base::unique_qptr(result.get()); } return result; } base::unique_qptr LinksController::createRowContextMenu( QWidget *parent, not_null row) { const auto real = static_cast(row.get()); const auto data = real->data(); const auto link = data.url; const auto copyLink = [=] { CopyInviteLink(delegate()->peerListUiShow(), link); }; const auto shareLink = [=] { delegate()->peerListUiShow()->showBox( ShareInviteLinkBox(&_window->session(), link)); }; const auto getLinkQr = [=] { delegate()->peerListUiShow()->showBox( InviteLinkQrBox(link, tr::lng_filters_link_qr_about())); }; const auto editLink = [=] { delegate()->peerListUiShow()->showBox( Box(ChatFilterLinkBox, &_window->session(), data)); }; const auto deleteLink = [=] { delegate()->peerListUiShow()->showBox(DeleteLinkBox(_window, data)); }; auto result = base::make_unique_q( parent, st::popupMenuWithIcons); result->addAction( tr::lng_group_invite_context_copy(tr::now), copyLink, &st::menuIconCopy); result->addAction( tr::lng_group_invite_context_share(tr::now), shareLink, &st::menuIconShare); result->addAction( tr::lng_group_invite_context_qr(tr::now), getLinkQr, &st::menuIconQrCode); result->addAction( tr::lng_filters_link_name_it(tr::now), editLink, &st::menuIconEdit); result->addAction( tr::lng_group_invite_context_delete(tr::now), deleteLink, &st::menuIconDelete); return result; } Main::Session &LinksController::session() const { return _window->session(); } void LinksController::appendRow(const InviteLinkData &data) { delegate()->peerListAppendRow(std::make_unique(this, data)); } void LinksController::rowUpdateRow(not_null row) { delegate()->peerListUpdateRow(row); } void LinksController::rowPaintIcon( QPainter &p, int x, int y, int size, Color color) { const auto skip = st::inviteLinkIconSkip; const auto inner = size - 2 * skip; const auto bg = [&] { switch (color) { case Color::Permanent: return &st::msgFile1Bg; } Unexpected("Color in LinksController::rowPaintIcon."); }(); auto &icon = _icons[int(color)]; if (icon.isNull()) { icon = QImage( QSize(inner, inner) * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied); icon.fill(Qt::transparent); icon.setDevicePixelRatio(style::DevicePixelRatio()); auto p = QPainter(&icon); p.setPen(Qt::NoPen); p.setBrush(*bg); { auto hq = PainterHighQualityEnabler(p); p.drawEllipse(QRect(0, 0, inner, inner)); } st::inviteLinkIcon.paintInCenter(p, { 0, 0, inner, inner }); } p.drawImage(x + skip, y + skip, icon); } } // namespace std::vector> CollectFilterLinkChats( const Data::ChatFilter &filter) { return filter.always() | ranges::views::filter([]( not_null history) { return !ErrorForSharing(history); }) | ranges::views::transform(&History::peer) | ranges::to_vector; } bool GoodForExportFilterLink( not_null window, const Data::ChatFilter &filter) { using Flag = Data::ChatFilter::Flag; const auto listflags = Flag::Chatlist | Flag::HasMyLinks; if (!filter.never().empty() || (filter.flags() & ~listflags)) { window->showToast(tr::lng_filters_link_cant(tr::now)); return false; } return true; } void ExportFilterLink( FilterId id, const std::vector> &peers, Fn done, Fn fail) { Expects(!peers.empty()); const auto front = peers.front(); const auto session = &front->session(); auto mtpPeers = peers | ranges::views::transform( [](not_null peer) { return MTPInputPeer(peer->input); } ) | ranges::to>(); session->api().request(MTPchatlists_ExportChatlistInvite( MTP_inputChatlistDialogFilter(MTP_int(id)), MTP_string(), // title MTP_vector(std::move(mtpPeers)) )).done([=](const MTPchatlists_ExportedChatlistInvite &result) { const auto &data = result.data(); session->data().chatsFilters().apply(MTP_updateDialogFilter( MTP_flags(MTPDupdateDialogFilter::Flag::f_filter), MTP_int(id), data.vfilter())); const auto link = session->data().chatsFilters().add( id, data.vinvite()); done(link); }).fail([=](const MTP::Error &error) { fail(error.type()); }).send(); } void EditLinkChats( const Data::ChatFilterLink &link, base::flat_set> peers, Fn done) { Expects(!peers.empty()); Expects(link.id != 0); Expects(!link.url.isEmpty()); const auto id = link.id; const auto front = peers.front(); const auto session = &front->session(); auto mtpPeers = peers | ranges::views::transform( [](not_null peer) { return MTPInputPeer(peer->input); } ) | ranges::to>(); session->api().request(MTPchatlists_EditExportedInvite( MTP_flags(MTPchatlists_EditExportedInvite::Flag::f_peers), MTP_inputChatlistDialogFilter(MTP_int(link.id)), MTP_string(link.url), MTPstring(), // title MTP_vector(std::move(mtpPeers)) )).done([=](const MTPExportedChatlistInvite &result) { const auto link = session->data().chatsFilters().add(id, result); done(QString()); }).fail([=](const MTP::Error &error) { done(error.type()); }).send(); } object_ptr ShowLinkBox( not_null window, const Data::ChatFilter &filter, const Data::ChatFilterLink &link) { auto controller = std::make_unique(window, filter, link); controller->setStyleOverrides(&st::inviteLinkChatList); const auto raw = controller.get(); auto initBox = [=](not_null box) { box->setTitle(!link.title.isEmpty() ? rpl::single(link.title) : tr::lng_filters_link_title()); const auto saving = std::make_shared(false); raw->hasChangesValue( ) | rpl::start_with_next([=](bool has) { box->setCloseByOutsideClick(!has); box->setCloseByEscape(!has); box->clearButtons(); if (has) { box->addButton(tr::lng_settings_save(), [=] { if (*saving) { return; } const auto chosen = raw->selected(); if (chosen.empty()) { ShowEmptyLinkError(window); } else { *saving = true; EditLinkChats(link, chosen, crl::guard(box, [=]( QString error) { *saving = false; if (error.isEmpty()) { box->closeBox(); } else { ShowSaveError(window, error); } })); } }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } else { box->addButton(tr::lng_about_done(), [=] { box->closeBox(); }); } }, box->lifetime()); }; return Box(std::move(controller), std::move(initBox)); } QString FilterChatStatusText(not_null peer) { if (const auto chat = peer->asChat()) { if (const auto count = chat->count; count > 0) { return tr::lng_chat_status_members(tr::now, lt_count, count); } } else if (const auto channel = peer->asChannel()) { if (channel->membersCountKnown()) { return (channel->isBroadcast() ? tr::lng_chat_status_subscribers : tr::lng_chat_status_members)( tr::now, lt_count, channel->membersCount()); } } return QString(); } void SetupFilterLinks( not_null container, not_null window, rpl::producer> value, Fn currentFilter) { auto &lifetime = container->lifetime(); const auto delegate = lifetime.make_state( window->uiShow()); const auto controller = lifetime.make_state( window, std::move(value), std::move(currentFilter)); controller->setStyleOverrides(&st::inviteLinkList); const auto content = container->add(object_ptr( container, controller)); delegate->setContent(content); controller->setDelegate(delegate); } void AddFilterSubtitleWithToggles( not_null container, rpl::producer text, int selectableCount, rpl::producer selectedCount, Fn toggle) { using namespace rpl::mappers; const auto selectable = (selectableCount > 0); auto padding = st::filterLinkSubsectionTitlePadding; if (selectable) { const auto font = st::boxLinkButton.font; padding.setRight(padding.right() + font->spacew + std::max( font->width(tr::lng_filters_by_link_select(tr::now)), font->width(tr::lng_filters_by_link_deselect(tr::now)))); } const auto title = Settings::AddSubsectionTitle( container, std::move(text), padding); if (!selectable) { return; } const auto link = Ui::CreateChild( container.get(), tr::lng_filters_by_link_select(tr::now), st::boxLinkButton); const auto canSelect = link->lifetime().make_state>( std::move(selectedCount) | rpl::map(_1 < selectableCount)); canSelect->value( ) | rpl::start_with_next([=](bool can) { link->setText(can ? tr::lng_filters_by_link_select(tr::now) : tr::lng_filters_by_link_deselect(tr::now)); }, link->lifetime()); link->setClickedCallback([=] { toggle(canSelect->current()); }); rpl::combine( container->widthValue(), title->topValue(), link->widthValue() ) | rpl::start_with_next([=](int outer, int y, int width) { link->move(outer - st::boxRowPadding.right() - width, y); }, link->lifetime()); } std::unique_ptr MakeFilterChatRow( not_null peer, const QString &status, bool disabled) { return std::make_unique(peer, status, disabled); }