1016 lines
		
	
	
	
		
			29 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1016 lines
		
	
	
	
		
			29 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| 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 "settings/settings_folders.h"
 | |
| 
 | |
| #include "kotato/kotato_lang.h"
 | |
| #include "kotato/kotato_settings.h"
 | |
| #include "apiwrap.h"
 | |
| #include "api/api_chat_filters.h" // ProcessFilterRemove.
 | |
| #include "boxes/premium_limits_box.h"
 | |
| #include "boxes/filters/edit_filter_box.h"
 | |
| #include "core/application.h"
 | |
| #include "data/data_chat_filters.h"
 | |
| #include "data/data_folder.h"
 | |
| #include "data/data_peer.h"
 | |
| #include "data/data_peer_values.h" // Data::AmPremiumValue.
 | |
| #include "data/data_session.h"
 | |
| #include "data/data_premium_limits.h"
 | |
| #include "history/history.h"
 | |
| #include "lang/lang_keys.h"
 | |
| #include "lottie/lottie_icon.h"
 | |
| #include "main/main_session.h"
 | |
| #include "ui/boxes/confirm_box.h"
 | |
| #include "ui/filter_icons.h"
 | |
| #include "main/main_account.h"
 | |
| #include "ui/toast/toast.h"
 | |
| #include "ui/layers/generic_box.h"
 | |
| #include "ui/painter.h"
 | |
| #include "ui/vertical_list.h"
 | |
| #include "ui/text/text_utilities.h"
 | |
| #include "ui/widgets/box_content_divider.h"
 | |
| #include "ui/widgets/buttons.h"
 | |
| #include "ui/widgets/fields/input_field.h"
 | |
| #include "ui/widgets/labels.h"
 | |
| #include "ui/wrap/slide_wrap.h"
 | |
| #include "window/window_controller.h"
 | |
| #include "window/window_session_controller.h"
 | |
| #include "styles/style_settings.h"
 | |
| #include "styles/style_layers.h"
 | |
| #include "styles/style_menu_icons.h"
 | |
| #include "styles/style_boxes.h"
 | |
| #include "styles/style_chat_helpers.h"
 | |
| #include "styles/style_window.h"
 | |
| 
 | |
| namespace Settings {
 | |
| namespace {
 | |
| 
 | |
| auto currentDefaultRemoved = false;
 | |
| 
 | |
| using Flag = Data::ChatFilter::Flag;
 | |
| using Flags = Data::ChatFilter::Flags;
 | |
| 
 | |
| class FilterRowButton final : public Ui::RippleButton {
 | |
| public:
 | |
| 	FilterRowButton(
 | |
| 		not_null<QWidget*> parent,
 | |
| 		not_null<Main::Session*> session,
 | |
| 		const Data::ChatFilter &filter);
 | |
| 	FilterRowButton(
 | |
| 		not_null<QWidget*> parent,
 | |
| 		const Data::ChatFilter &filter,
 | |
| 		const QString &description);
 | |
| 
 | |
| 	void setRemoved(bool removed);
 | |
| 	void updateData(const Data::ChatFilter &filter);
 | |
| 	void updateCount(const Data::ChatFilter &filter);
 | |
| 
 | |
| 	[[nodiscard]] rpl::producer<> removeRequests() const;
 | |
| 	[[nodiscard]] rpl::producer<> restoreRequests() const;
 | |
| 	[[nodiscard]] rpl::producer<> addRequests() const;
 | |
| 
 | |
| private:
 | |
| 	enum class State {
 | |
| 		Suggested,
 | |
| 		Removed,
 | |
| 		Normal,
 | |
| 	};
 | |
| 
 | |
| 	FilterRowButton(
 | |
| 		not_null<QWidget*> parent,
 | |
| 		Main::Session *session,
 | |
| 		const Data::ChatFilter &filter,
 | |
| 		const QString &description,
 | |
| 		State state);
 | |
| 
 | |
| 	void paintEvent(QPaintEvent *e) override;
 | |
| 
 | |
| 	void setup(const Data::ChatFilter &filter, const QString &status);
 | |
| 	void setState(State state, bool force = false);
 | |
| 	void updateButtonsVisibility();
 | |
| 
 | |
| 	Main::Session *_session = nullptr;
 | |
| 
 | |
| 	Ui::IconButton _remove;
 | |
| 	Ui::RoundButton _restore;
 | |
| 	Ui::RoundButton _add;
 | |
| 
 | |
| 	Ui::Text::String _title;
 | |
| 	QString _status;
 | |
| 	Ui::FilterIcon _icon = Ui::FilterIcon();
 | |
| 
 | |
| 	State _state = State::Normal;
 | |
| 
 | |
| };
 | |
| 
 | |
| struct FilterRow {
 | |
| 	not_null<FilterRowButton*> button;
 | |
| 	Data::ChatFilter filter;
 | |
| 	bool removed = false;
 | |
| 	mtpRequestId removePeersRequestId = 0;
 | |
| 	std::vector<not_null<PeerData*>> suggestRemovePeers;
 | |
| 	std::vector<not_null<PeerData*>> removePeers;
 | |
| 	bool added = false;
 | |
| 	bool postponedCountUpdate = false;
 | |
| };
 | |
| 
 | |
| [[nodiscard]] int CountFilterChats(
 | |
| 		not_null<Main::Session*> session,
 | |
| 		const Data::ChatFilter &filter) {
 | |
| 	auto result = 0;
 | |
| 	const auto addList = [&](not_null<Dialogs::MainList*> list) {
 | |
| 		for (const auto &entry : list->indexed()->all()) {
 | |
| 			if (const auto history = entry->history()) {
 | |
| 				if (filter.contains(history)) {
 | |
| 					++result;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	};
 | |
| 	addList(session->data().chatsList());
 | |
| 	const auto folderId = Data::Folder::kId;
 | |
| 	if (const auto folder = session->data().folderLoaded(folderId)) {
 | |
| 		addList(folder->chatsList());
 | |
| 	}
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| [[nodiscard]] int ComputeCount(
 | |
| 		not_null<Main::Session*> session,
 | |
| 		const Data::ChatFilter &filter,
 | |
| 		bool check = false) {
 | |
| 	const auto &list = session->data().chatsFilters().list();
 | |
| 	const auto id = filter.id();
 | |
| 	const auto i = ranges::find(list, id, &Data::ChatFilter::id);
 | |
| 	if ((id && i != end(list))
 | |
| 		&& (!check
 | |
| 			|| (i->flags() == filter.flags()
 | |
| 				&& i->always() == filter.always()
 | |
| 				&& i->never() == filter.never()))) {
 | |
| 		const auto chats = session->data().chatsFilters().chatsList(id);
 | |
| 		return chats->indexed()->size();
 | |
| 	}
 | |
| 	return CountFilterChats(session, filter);
 | |
| }
 | |
| 
 | |
| [[nodiscard]] QString ComputeCountString(
 | |
| 		not_null<Main::Session*> session,
 | |
| 		const Data::ChatFilter &filter,
 | |
| 		bool check = false) {
 | |
| 	const auto count = ComputeCount(session, filter, check);
 | |
| 	const auto result = count
 | |
| 		? tr::lng_filters_chats_count(tr::now, lt_count_short, count)
 | |
| 		: tr::lng_filters_no_chats(tr::now);
 | |
| 	return filter.chatlist()
 | |
| 		? (result
 | |
| 			+ QString::fromUtf8(" \xE2\x80\xA2 ")
 | |
| 			+ tr::lng_filters_shareable_status(tr::now))
 | |
| 		: (result
 | |
| 			+ QString::fromUtf8(" \xE2\x80\xA2 ")
 | |
| 			+ (filter.isLocal()
 | |
| 				? ktr("ktg_filters_local")
 | |
| 				: ktr("ktg_filters_cloud")));
 | |
| }
 | |
| 
 | |
| FilterRowButton::FilterRowButton(
 | |
| 	not_null<QWidget*> parent,
 | |
| 	not_null<Main::Session*> session,
 | |
| 	const Data::ChatFilter &filter)
 | |
| : FilterRowButton(
 | |
| 	parent,
 | |
| 	session,
 | |
| 	filter,
 | |
| 	ComputeCountString(session, filter),
 | |
| 	State::Normal) {
 | |
| }
 | |
| 
 | |
| FilterRowButton::FilterRowButton(
 | |
| 	not_null<QWidget*> parent,
 | |
| 	const Data::ChatFilter &filter,
 | |
| 	const QString &description)
 | |
| : FilterRowButton(parent, nullptr, filter, description, State::Suggested) {
 | |
| }
 | |
| 
 | |
| FilterRowButton::FilterRowButton(
 | |
| 	not_null<QWidget*> parent,
 | |
| 	Main::Session *session,
 | |
| 	const Data::ChatFilter &filter,
 | |
| 	const QString &status,
 | |
| 	State state)
 | |
| : RippleButton(parent, st::defaultRippleAnimation)
 | |
| , _session(session)
 | |
| , _remove(this, st::filtersRemove)
 | |
| , _restore(this, tr::lng_filters_restore(), st::stickersUndoRemove)
 | |
| , _add(this, tr::lng_filters_recommended_add(), st::stickersTrendingAdd)
 | |
| , _state(state) {
 | |
| 	_restore.setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
 | |
| 	_add.setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
 | |
| 	setup(filter, status);
 | |
| }
 | |
| 
 | |
| void FilterRowButton::setRemoved(bool removed) {
 | |
| 	setState(removed ? State::Removed : State::Normal);
 | |
| }
 | |
| 
 | |
| void FilterRowButton::updateData(const Data::ChatFilter &filter) {
 | |
| 	Expects(_session != nullptr);
 | |
| 
 | |
| 	_title.setText(st::contactsNameStyle, filter.title());
 | |
| 	_icon = Ui::ComputeFilterIcon(filter);
 | |
| 	updateCount(filter);
 | |
| }
 | |
| 
 | |
| void FilterRowButton::updateCount(const Data::ChatFilter &filter) {
 | |
| 	_status = ComputeCountString(_session, filter, true);
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void FilterRowButton::setState(State state, bool force) {
 | |
| 	if (!force && _state == state) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_state = state;
 | |
| 	setPointerCursor(_state == State::Normal);
 | |
| 	setDisabled(_state != State::Normal);
 | |
| 	updateButtonsVisibility();
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void FilterRowButton::setup(
 | |
| 		const Data::ChatFilter &filter,
 | |
| 		const QString &status) {
 | |
| 	resize(width(), st::defaultPeerListItem.height);
 | |
| 
 | |
| 	_title.setText(st::contactsNameStyle, filter.title());
 | |
| 	_status = status;
 | |
| 	_icon = Ui::ComputeFilterIcon(filter);
 | |
| 
 | |
| 	setState(_state, true);
 | |
| 
 | |
| 	sizeValue(
 | |
| 	) | rpl::start_with_next([=](QSize size) {
 | |
| 		const auto right = st::contactsPadding.right()
 | |
| 			+ st::contactsCheckPosition.x();
 | |
| 		const auto width = size.width();
 | |
| 		const auto height = size.height();
 | |
| 		_restore.moveToRight(right, (height - _restore.height()) / 2, width);
 | |
| 		_add.moveToRight(right, (height - _add.height()) / 2, width);
 | |
| 		const auto skipped = right - st::stickersRemoveSkip;
 | |
| 		_remove.moveToRight(skipped, (height - _remove.height()) / 2, width);
 | |
| 	}, lifetime());
 | |
| }
 | |
| 
 | |
| void FilterRowButton::updateButtonsVisibility() {
 | |
| 	_remove.setVisible(_state == State::Normal);
 | |
| 	_restore.setVisible(_state == State::Removed);
 | |
| 	_add.setVisible(_state == State::Suggested);
 | |
| }
 | |
| 
 | |
| rpl::producer<> FilterRowButton::removeRequests() const {
 | |
| 	return _remove.clicks() | rpl::to_empty;
 | |
| }
 | |
| 
 | |
| rpl::producer<> FilterRowButton::restoreRequests() const {
 | |
| 	return _restore.clicks() | rpl::to_empty;
 | |
| }
 | |
| 
 | |
| rpl::producer<> FilterRowButton::addRequests() const {
 | |
| 	return _add.clicks() | rpl::to_empty;
 | |
| }
 | |
| 
 | |
| void FilterRowButton::paintEvent(QPaintEvent *e) {
 | |
| 	auto p = Painter(this);
 | |
| 
 | |
| 	const auto over = isOver() || isDown();
 | |
| 	if (_state == State::Normal) {
 | |
| 		if (over) {
 | |
| 			p.fillRect(e->rect(), st::windowBgOver);
 | |
| 		}
 | |
| 		RippleButton::paintRipple(p, 0, 0);
 | |
| 	} else if (_state == State::Removed) {
 | |
| 		p.setOpacity(st::stickersRowDisabledOpacity);
 | |
| 	}
 | |
| 
 | |
| 	const auto left = (_state == State::Suggested)
 | |
| 		? st::defaultSubsectionTitlePadding.left()
 | |
| 		: st::settingsButtonActive.padding.left();
 | |
| 	const auto buttonsLeft = std::min(
 | |
| 		_add.x(),
 | |
| 		std::min(_remove.x(), _restore.x()));
 | |
| 	const auto availableWidth = buttonsLeft - left;
 | |
| 
 | |
| 	p.setPen(st::contactsNameFg);
 | |
| 	_title.drawLeftElided(
 | |
| 		p,
 | |
| 		left,
 | |
| 		st::contactsPadding.top() + st::contactsNameTop,
 | |
| 		availableWidth,
 | |
| 		width());
 | |
| 
 | |
| 	p.setFont(st::contactsStatusFont);
 | |
| 	p.setPen(st::contactsStatusFg);
 | |
| 	p.drawTextLeft(
 | |
| 		left,
 | |
| 		st::contactsPadding.top() + st::contactsStatusTop,
 | |
| 		width(),
 | |
| 		_status);
 | |
| 
 | |
| 	if (_state != State::Suggested) {
 | |
| 		const auto icon = Ui::LookupFilterIcon(_icon).normal;
 | |
| 
 | |
| 		// For now.
 | |
| 		auto hq = PainterHighQualityEnabler(p);
 | |
| 		const auto iconWidth = icon->width() - style::ConvertScale(9);
 | |
| 		const auto scale = st::settingsIconAdd.width() / float64(iconWidth);
 | |
| 		p.translate(
 | |
| 			st::settingsButtonActive.iconLeft,
 | |
| 			(height() - icon->height() * scale) / 2);
 | |
| 		p.translate(-iconWidth / 2, -iconWidth / 2);
 | |
| 		p.scale(scale, scale);
 | |
| 		p.translate(iconWidth / 2, iconWidth / 2);
 | |
| 		icon->paint(
 | |
| 			p,
 | |
| 			0,
 | |
| 			0,
 | |
| 			width(),
 | |
| 			(over
 | |
| 				? st::activeButtonBgOver
 | |
| 				: st::activeButtonBg)->c);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| [[nodiscard]] Fn<void()> SetupFoldersContent(
 | |
| 		not_null<Window::SessionController*> controller,
 | |
| 		not_null<Ui::VerticalLayout*> container) {
 | |
| 	auto &lifetime = container->lifetime();
 | |
| 
 | |
| 	const auto weak = Ui::MakeWeak(container);
 | |
| 	const auto session = &controller->session();
 | |
| 	const auto limit = [=] {
 | |
| 		return Data::PremiumLimits(session).dialogFiltersCurrent();
 | |
| 	};
 | |
| 	const auto account = &session->account();
 | |
| 	const auto currentDefaultId = account->defaultFilterId();
 | |
| 	auto localNewFilterId = limit();
 | |
| 	const auto generateNewId = [=, &localNewFilterId] {
 | |
| 		const auto filters = &controller->session().data().chatsFilters();
 | |
| 
 | |
| 		do {
 | |
| 			localNewFilterId++;
 | |
| 		} while (ranges::contains(filters->list(), localNewFilterId, &Data::ChatFilter::id));
 | |
| 
 | |
| 		return localNewFilterId;
 | |
| 	};
 | |
| 
 | |
| 	currentDefaultRemoved = false;
 | |
| 
 | |
| 	Ui::AddSkip(container, st::defaultVerticalListSkip);
 | |
| 	Ui::AddSubsectionTitle(container, tr::lng_filters_subtitle());
 | |
| 
 | |
| 	struct State {
 | |
| 		std::vector<FilterRow> rows;
 | |
| 		rpl::variable<int> count;
 | |
| 		rpl::variable<int> suggested;
 | |
| 		Fn<void(const FilterRowButton*, Fn<void(Data::ChatFilter)>)> save;
 | |
| 	};
 | |
| 
 | |
| 	const auto state = lifetime.make_state<State>();
 | |
| 	const auto find = [=](not_null<FilterRowButton*> button) {
 | |
| 		const auto i = ranges::find(state->rows, button, &FilterRow::button);
 | |
| 		Assert(i != end(state->rows));
 | |
| 		return &*i;
 | |
| 	};
 | |
| 	const auto toast = Ui::Toast::Config{
 | |
| 		.text = { ktr("ktg_filters_cloud_limit") },
 | |
| 		.st = &st::windowArchiveToast,
 | |
| 		.multiline = true,
 | |
| 	};
 | |
| 	const auto showLimitReached = [=] {
 | |
| 		const auto removed = ranges::count_if(state->rows, [](FilterRow row) {
 | |
| 			return row.removed || row.filter.isLocal();
 | |
| 		});
 | |
| 		const auto count = int(state->rows.size() - removed);
 | |
| 		if (count < limit()) {
 | |
| 			return false;
 | |
| 		}
 | |
| 		controller->show(Box(FiltersLimitBox, session, count));
 | |
| 		return true;
 | |
| 	};
 | |
| 	const auto newCloudButton = AddButtonWithIcon(
 | |
| 		container,
 | |
| 		rktr("ktg_filters_create_cloud"),
 | |
| 		st::settingsButton,
 | |
| 		{ &st::settingsIconCloud }
 | |
| 	);
 | |
| 	const auto newLocalButton = AddButtonWithIcon(
 | |
| 		container,
 | |
| 		rktr("ktg_filters_create_local"),
 | |
| 		st::settingsButton,
 | |
| 		{ &st::menuIconShowInFolder }
 | |
| 	);
 | |
| 	const auto markForRemovalSure = [=](not_null<FilterRowButton*> button) {
 | |
| 		const auto row = find(button);
 | |
| 		auto suggestRemoving = Api::ExtractSuggestRemoving(row->filter);
 | |
| 		if (row->removed || row->removePeersRequestId > 0) {
 | |
| 			return;
 | |
| 		} else if (!suggestRemoving.empty()) {
 | |
| 			const auto chosen = crl::guard(button, [=](
 | |
| 					std::vector<not_null<PeerData*>> peers) {
 | |
| 				const auto row = find(button);
 | |
| 				row->removePeers = std::move(peers);
 | |
| 				row->removed = true;
 | |
| 				button->setRemoved(true);
 | |
| 			});
 | |
| 			Api::ProcessFilterRemove(
 | |
| 				controller,
 | |
| 				row->filter.title(),
 | |
| 				row->filter.iconEmoji(),
 | |
| 				std::move(suggestRemoving),
 | |
| 				row->suggestRemovePeers,
 | |
| 				chosen);
 | |
| 		} else {
 | |
| 			row->removePeers = {};
 | |
| 			row->removed = true;
 | |
| 			button->setRemoved(true);
 | |
| 		}
 | |
| 	};
 | |
| 	const auto markForRemoval = [=](not_null<FilterRowButton*> button) {
 | |
| 		const auto row = find(button);
 | |
| 		if (row->removed || row->removePeersRequestId > 0) {
 | |
| 			return;
 | |
| 		} else if (row->filter.hasMyLinks()) {
 | |
| 			controller->show(Ui::MakeConfirmBox({
 | |
| 				.text = { tr::lng_filters_delete_sure(tr::now) },
 | |
| 				.confirmed = crl::guard(button, [=](Fn<void()> close) {
 | |
| 					markForRemovalSure(button);
 | |
| 					close();
 | |
| 				}),
 | |
| 				.confirmText = tr::lng_box_delete(),
 | |
| 				.confirmStyle = &st::attentionBoxButton,
 | |
| 			}));
 | |
| 		} else {
 | |
| 			markForRemovalSure(button);
 | |
| 		}
 | |
| 	};
 | |
| 	const auto remove = [=](not_null<FilterRowButton*> button) {
 | |
| 		const auto row = find(button);
 | |
| 		if (row->removed || row->removePeersRequestId > 0) {
 | |
| 			return;
 | |
| 		} else if (row->filter.chatlist() && !row->removePeersRequestId) {
 | |
| 			row->removePeersRequestId = session->api().request(
 | |
| 				MTPchatlists_GetLeaveChatlistSuggestions(
 | |
| 					MTP_inputChatlistDialogFilter(
 | |
| 						MTP_int(row->filter.id())))
 | |
| 			).done(crl::guard(button, [=](const MTPVector<MTPPeer> &result) {
 | |
| 				const auto row = find(button);
 | |
| 				row->removePeersRequestId = -1;
 | |
| 				row->suggestRemovePeers = ranges::views::all(
 | |
| 					result.v
 | |
| 				) | ranges::views::transform([=](const MTPPeer &peer) {
 | |
| 					return session->data().peer(peerFromMTP(peer));
 | |
| 				}) | ranges::to_vector;
 | |
| 				markForRemoval(button);
 | |
| 			})).fail(crl::guard(button, [=] {
 | |
| 				const auto row = find(button);
 | |
| 				row->removePeersRequestId = -1;
 | |
| 				markForRemoval(button);
 | |
| 			})).send();
 | |
| 		} else {
 | |
| 			markForRemoval(button);
 | |
| 		}
 | |
| 	};
 | |
| 	const auto wrap = container->add(object_ptr<Ui::VerticalLayout>(
 | |
| 		container));
 | |
| 	const auto addFilter = [=](const Data::ChatFilter &filter) {
 | |
| 		if (state->rows.size() == 0) {
 | |
| 			AddSkip(wrap);
 | |
| 			AddDivider(wrap);
 | |
| 			AddSkip(wrap);
 | |
| 		}
 | |
| 		const auto button = wrap->add(
 | |
| 			object_ptr<FilterRowButton>(wrap, session, filter));
 | |
| 		button->removeRequests(
 | |
| 		) | rpl::start_with_next([=] {
 | |
| 			remove(button);
 | |
| 			if (find(button)->filter.id() == account->defaultFilterId()) {
 | |
| 				currentDefaultRemoved = true;
 | |
| 			}
 | |
| 		}, button->lifetime());
 | |
| 		button->restoreRequests(
 | |
| 		) | rpl::start_with_next([=] {
 | |
| 			if (showLimitReached()) {
 | |
| 				return;
 | |
| 			}
 | |
| 			if (find(button)->filter.id() == account->defaultFilterId()) {
 | |
| 				currentDefaultRemoved = false;
 | |
| 			}
 | |
| 			button->setRemoved(false);
 | |
| 			find(button)->removed = false;
 | |
| 		}, button->lifetime());
 | |
| 		button->setClickedCallback([=] {
 | |
| 			const auto found = find(button);
 | |
| 			if (found->removed) {
 | |
| 				return;
 | |
| 			}
 | |
| 			const auto doneCallback = [=](const Data::ChatFilter &result) {
 | |
| 				find(button)->filter = result;
 | |
| 				const auto isCurrentDefault = result.id() == account->defaultFilterId();
 | |
| 				if ((isCurrentDefault && !result.isDefault())
 | |
| 					|| (!isCurrentDefault && result.isDefault())) {
 | |
| 					account->setDefaultFilterId(result.isDefault() ? result.id() : 0);
 | |
| 				}
 | |
| 				button->updateData(result);
 | |
| 			};
 | |
| 			const auto saveAnd = [=](
 | |
| 					const Data::ChatFilter &data,
 | |
| 					Fn<void(Data::ChatFilter)> next) {
 | |
| 				doneCallback(data);
 | |
| 				state->save(button, next);
 | |
| 			};
 | |
| 			controller->window().show(Box(
 | |
| 				EditFilterBox,
 | |
| 				controller,
 | |
| 				found->filter,
 | |
| 				crl::guard(button, doneCallback),
 | |
| 				crl::guard(button, saveAnd)));
 | |
| 		});
 | |
| 		state->rows.push_back({ button, filter });
 | |
| 		state->count = state->rows.size();
 | |
| 
 | |
| 		const auto filters = &controller->session().data().chatsFilters();
 | |
| 		const auto id = filter.id();
 | |
| 		if (ranges::contains(filters->list(), id, &Data::ChatFilter::id)) {
 | |
| 			filters->chatsList(id)->fullSize().changes(
 | |
| 			) | rpl::start_with_next([=] {
 | |
| 				const auto found = find(button);
 | |
| 				if (found->postponedCountUpdate) {
 | |
| 					return;
 | |
| 				}
 | |
| 				found->postponedCountUpdate = true;
 | |
| 				Ui::PostponeCall(button, [=] {
 | |
| 					const auto &list = filters->list();
 | |
| 					const auto i = ranges::find(
 | |
| 						list,
 | |
| 						id,
 | |
| 						&Data::ChatFilter::id);
 | |
| 					if (i == end(list)) {
 | |
| 						return;
 | |
| 					}
 | |
| 					const auto found = find(button);
 | |
| 					const auto &now = found->filter;
 | |
| 					if ((i->flags() != now.flags())
 | |
| 						|| (i->always() != now.always())
 | |
| 						|| (i->never() != now.never())) {
 | |
| 						return;
 | |
| 					}
 | |
| 					button->updateCount(now);
 | |
| 					found->postponedCountUpdate = false;
 | |
| 				});
 | |
| 			}, button->lifetime());
 | |
| 		}
 | |
| 
 | |
| 		wrap->resizeToWidth(container->width());
 | |
| 
 | |
| 		return button;
 | |
| 	};
 | |
| 	const auto &list = session->data().chatsFilters().list();
 | |
| 	for (const auto &filter : list) {
 | |
| 		if (filter.id()) {
 | |
| 			addFilter(filter);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	session->data().chatsFilters().isChatlistChanged(
 | |
| 	) | rpl::start_with_next([=](FilterId id) {
 | |
| 		const auto filters = &session->data().chatsFilters();
 | |
| 		const auto &list = filters->list();
 | |
| 		const auto i = ranges::find(list, id, &Data::ChatFilter::id);
 | |
| 		const auto j = ranges::find(state->rows, id, [](const auto &row) {
 | |
| 			return row.filter.id();
 | |
| 		});
 | |
| 		if (i == end(list) || j == end(state->rows)) {
 | |
| 			return;
 | |
| 		}
 | |
| 		j->filter = j->filter.withChatlist(i->chatlist(), i->hasMyLinks());
 | |
| 		j->button->updateCount(j->filter);
 | |
| 	}, container->lifetime());
 | |
| 
 | |
| 	newCloudButton->setClickedCallback([=] {
 | |
| 		if (showLimitReached()) {
 | |
| 			return;
 | |
| 		}
 | |
| 		const auto created = std::make_shared<FilterRowButton*>(nullptr);
 | |
| 		const auto doneCallback = [=](const Data::ChatFilter &result) {
 | |
| 			if (result.isDefault()) {
 | |
| 				account->setDefaultFilterId(result.id());
 | |
| 			}
 | |
| 			if (const auto button = *created) {
 | |
| 				find(button)->filter = result;
 | |
| 				button->updateData(result);
 | |
| 			} else {
 | |
| 				*created = addFilter(result);
 | |
| 			}
 | |
| 		};
 | |
| 		const auto saveAnd = [=](
 | |
| 				const Data::ChatFilter &data,
 | |
| 				Fn<void(Data::ChatFilter)> next) {
 | |
| 			doneCallback(data);
 | |
| 			state->save(*created, next);
 | |
| 		};
 | |
| 		controller->window().show(Box(
 | |
| 			EditFilterBox,
 | |
| 			controller,
 | |
| 			Data::ChatFilter(generateNewId()),
 | |
| 			crl::guard(container, doneCallback),
 | |
| 			crl::guard(container, saveAnd)));
 | |
| 	});
 | |
| 	newLocalButton->setClickedCallback([=] {
 | |
| 		const auto created = std::make_shared<FilterRowButton*>(nullptr);
 | |
| 		const auto doneCallback = [=](const Data::ChatFilter &result) {
 | |
| 			if (result.isDefault()) {
 | |
| 				account->setDefaultFilterId(result.id());
 | |
| 			}
 | |
| 			if (const auto button = *created) {
 | |
| 				find(button)->filter = result;
 | |
| 				button->updateData(result);
 | |
| 			} else {
 | |
| 				*created = addFilter(result);
 | |
| 			}
 | |
| 		};
 | |
| 		const auto saveAnd = [=](
 | |
| 				const Data::ChatFilter &data,
 | |
| 				Fn<void(Data::ChatFilter)> next) {
 | |
| 			doneCallback(data);
 | |
| 			state->save(*created, next);
 | |
| 		};
 | |
| 		controller->window().show(Box(
 | |
| 			EditFilterBox,
 | |
| 			controller,
 | |
| 			Data::ChatFilter(generateNewId(), true),
 | |
| 			crl::guard(container, doneCallback),
 | |
| 			crl::guard(container, saveAnd)));
 | |
| 	});
 | |
| 	Ui::AddSkip(container);
 | |
| 	const auto nonEmptyAbout = container->add(
 | |
| 		object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
 | |
| 			container,
 | |
| 			object_ptr<Ui::VerticalLayout>(container))
 | |
| 	)->setDuration(0);
 | |
| 	const auto aboutRows = nonEmptyAbout->entity();
 | |
| 	Ui::AddDividerText(aboutRows, rktr("ktg_filters_description"));
 | |
| 	Ui::AddSkip(aboutRows);
 | |
| 	Ui::AddSubsectionTitle(aboutRows, tr::lng_filters_recommended());
 | |
| 
 | |
| 	rpl::single(rpl::empty) | rpl::then(
 | |
| 		session->data().chatsFilters().suggestedUpdated()
 | |
| 	) | rpl::map([=] {
 | |
| 		return session->data().chatsFilters().suggestedFilters();
 | |
| 	}) | rpl::filter([=](const std::vector<Data::SuggestedFilter> &list) {
 | |
| 		return !list.empty();
 | |
| 	}) | rpl::take(
 | |
| 		1
 | |
| 	) | rpl::start_with_next([=](
 | |
| 			const std::vector<Data::SuggestedFilter> &suggestions) {
 | |
| 		for (const auto &suggestion : suggestions) {
 | |
| 			const auto &filter = suggestion.filter;
 | |
| 			if (ranges::contains(state->rows, filter, &FilterRow::filter)) {
 | |
| 				continue;
 | |
| 			}
 | |
| 			state->suggested = state->suggested.current() + 1;
 | |
| 			const auto button = aboutRows->add(object_ptr<FilterRowButton>(
 | |
| 				aboutRows,
 | |
| 				filter,
 | |
| 				suggestion.description));
 | |
| 			button->addRequests(
 | |
| 				) | rpl::start_with_next([=] {
 | |
| 				if (showLimitReached()) {
 | |
| 					return;
 | |
| 				}
 | |
| 				addFilter(filter);
 | |
| 				state->suggested = state->suggested.current() - 1;
 | |
| 				delete button;
 | |
| 			}, button->lifetime());
 | |
| 		}
 | |
| 		aboutRows->resizeToWidth(container->width());
 | |
| 		Ui::AddSkip(aboutRows, st::defaultVerticalListSkip);
 | |
| 	}, aboutRows->lifetime());
 | |
| 
 | |
| 	auto showSuggestions = rpl::combine(
 | |
| 		state->suggested.value(),
 | |
| 		state->count.value(),
 | |
| 		Data::AmPremiumValue(session)
 | |
| 	) | rpl::map([limit](int suggested, int count, bool) {
 | |
| 		return suggested > 0 && count < limit();
 | |
| 	});
 | |
| 	nonEmptyAbout->toggleOn(std::move(showSuggestions));
 | |
| 
 | |
| 	const auto prepareGoodIdsForNewFilters = [=] {
 | |
| 		const auto &list = session->data().chatsFilters().list();
 | |
| 
 | |
| 		auto localId = 1;
 | |
| 		const auto chooseNextId = [&] {
 | |
| 			++localId;
 | |
| 			while (ranges::contains(list, localId, &Data::ChatFilter::id)) {
 | |
| 				++localId;
 | |
| 			}
 | |
| 			return localId;
 | |
| 		};
 | |
| 		auto result = base::flat_map<not_null<FilterRowButton*>, FilterId>();
 | |
| 		for (auto &row : state->rows) {
 | |
| 			const auto id = row.filter.id();
 | |
| 			if (row.removed || row.filter.isLocal()) {
 | |
| 				continue;
 | |
| 			} else if (!id
 | |
| 				|| !ranges::contains(list, id, &Data::ChatFilter::id)) {
 | |
| 				result.emplace(row.button, chooseNextId());
 | |
| 				if (account->defaultFilterId() == id) {
 | |
| 					account->setDefaultFilterId(localId);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// We're prioritizing cloud IDs before local.
 | |
| 		localId = limit();
 | |
| 		for (auto &row : state->rows) {
 | |
| 			const auto id = row.filter.id();
 | |
| 			if (row.removed || !row.filter.isLocal()) {
 | |
| 				continue;
 | |
| 			} else if (!ranges::contains(list, id, &Data::ChatFilter::id)) {
 | |
| 				result.emplace(row.button, chooseNextId());
 | |
| 				if (account->defaultFilterId() == id) {
 | |
| 					account->setDefaultFilterId(localId);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		return result;
 | |
| 	};
 | |
| 
 | |
| 	state->save = [=](
 | |
| 			const FilterRowButton *single,
 | |
| 			Fn<void(Data::ChatFilter)> next) {
 | |
| 		auto ids = prepareGoodIdsForNewFilters();
 | |
| 		bool needSave = false;
 | |
| 
 | |
| 		auto updated = Data::ChatFilter();
 | |
| 
 | |
| 		auto order = std::vector<FilterId>();
 | |
| 		auto updates = std::vector<MTPUpdate>();
 | |
| 		auto addRequests = std::vector<MTPmessages_UpdateDialogFilter>();
 | |
| 		auto removeRequests = std::vector<MTPmessages_UpdateDialogFilter>();
 | |
| 		auto removeChatlistRequests = std::vector<MTPchatlists_LeaveChatlist>();
 | |
| 
 | |
| 		auto &realFilters = session->data().chatsFilters();
 | |
| 		const auto &list = realFilters.list();
 | |
| 		order.reserve(state->rows.size());
 | |
| 		auto localFoldersChanged = false;
 | |
| 		for (auto &row : state->rows) {
 | |
| 			if (row.button.get() == single) {
 | |
| 				updated = row.filter;
 | |
| 			}
 | |
| 			const auto id = row.filter.id();
 | |
| 			const auto removed = row.removed;
 | |
| 			const auto i = ranges::find(list, id, &Data::ChatFilter::id);
 | |
| 			if (removed && (i == end(list) || id == FilterId(0))) {
 | |
| 				continue;
 | |
| 			} else if (!removed && i != end(list) && *i == row.filter) {
 | |
| 				order.push_back(id);
 | |
| 				continue;
 | |
| 			}
 | |
| 			const auto newId = ids.take(row.button).value_or(id);
 | |
| 			if (newId != id) {
 | |
| 				row.filter = row.filter.withId(newId);
 | |
| 				row.button->updateData(row.filter);
 | |
| 				if (row.button.get() == single) {
 | |
| 					updated = row.filter;
 | |
| 				}
 | |
| 			}
 | |
| 			const auto tl = removed
 | |
| 				? MTPDialogFilter()
 | |
| 				: row.filter.tl(newId);
 | |
| 			const auto removeChatlistWithChats = removed
 | |
| 				&& row.filter.chatlist()
 | |
| 				&& !row.removePeers.empty();
 | |
| 			if (row.filter.isLocal()) {
 | |
| 				if (removed) {
 | |
| 					realFilters.remove(id);
 | |
| 				} else {
 | |
| 					realFilters.set(row.filter);
 | |
| 					order.push_back(id);
 | |
| 				}
 | |
| 				localFoldersChanged = true;
 | |
| 				needSave = true;
 | |
| 			} else if (removeChatlistWithChats) {
 | |
| 				auto inputs = ranges::views::all(
 | |
| 					row.removePeers
 | |
| 				) | ranges::views::transform([](not_null<PeerData*> peer) {
 | |
| 					return MTPInputPeer(peer->input);
 | |
| 				}) | ranges::to<QVector<MTPInputPeer>>();
 | |
| 				removeChatlistRequests.push_back(
 | |
| 					MTPchatlists_LeaveChatlist(
 | |
| 						MTP_inputChatlistDialogFilter(MTP_int(newId)),
 | |
| 						MTP_vector<MTPInputPeer>(std::move(inputs))));
 | |
| 			} else {
 | |
| 				const auto request = MTPmessages_UpdateDialogFilter(
 | |
| 					MTP_flags(removed
 | |
| 						? MTPmessages_UpdateDialogFilter::Flag(0)
 | |
| 						: MTPmessages_UpdateDialogFilter::Flag::f_filter),
 | |
| 					MTP_int(newId),
 | |
| 					tl);
 | |
| 				if (removed) {
 | |
| 					removeRequests.push_back(request);
 | |
| 				} else {
 | |
| 					addRequests.push_back(request);
 | |
| 					order.push_back(newId);
 | |
| 				}
 | |
| 				realFilters.apply(MTP_updateDialogFilter(
 | |
| 					MTP_flags(removed
 | |
| 						? MTPDupdateDialogFilter::Flag(0)
 | |
| 						: MTPDupdateDialogFilter::Flag::f_filter),
 | |
| 					MTP_int(newId),
 | |
| 					tl));
 | |
| 			}
 | |
| 			updates.push_back(MTP_updateDialogFilter(
 | |
| 				MTP_flags(removed
 | |
| 					? MTPDupdateDialogFilter::Flag(0)
 | |
| 					: MTPDupdateDialogFilter::Flag::f_filter),
 | |
| 				MTP_int(newId),
 | |
| 				tl));
 | |
| 		}
 | |
| 		if (!ranges::contains(order, FilterId(0))) {
 | |
| 			auto position = 0;
 | |
| 			for (const auto &filter : list) {
 | |
| 				const auto id = filter.id();
 | |
| 				if (!id) {
 | |
| 					break;
 | |
| 				} else if (const auto i = ranges::find(order, id)
 | |
| 					; i != order.end()) {
 | |
| 					position = int(i - order.begin()) + 1;
 | |
| 				}
 | |
| 			}
 | |
| 			order.insert(order.begin() + position, FilterId(0));
 | |
| 		}
 | |
| 		if (next) {
 | |
| 			// We're not closing the layer yet, so delete removed rows.
 | |
| 			for (auto i = state->rows.begin(); i != state->rows.end();) {
 | |
| 				if (i->removed) {
 | |
| 					const auto button = i->button;
 | |
| 					i = state->rows.erase(i);
 | |
| 					delete button;
 | |
| 				} else {
 | |
| 					++i;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		crl::on_main(session, [
 | |
| 			session,
 | |
| 			next,
 | |
| 			updated,
 | |
| 			account,
 | |
| 			controller,
 | |
| 			localFoldersChanged,
 | |
| 			currentDefaultId,
 | |
| 			&needSave,
 | |
| 			order = std::move(order),
 | |
| 			updates = std::move(updates),
 | |
| 			addRequests = std::move(addRequests),
 | |
| 			removeRequests = std::move(removeRequests),
 | |
| 			removeChatlistRequests = std::move(removeChatlistRequests)
 | |
| 		] {
 | |
| 			const auto api = &session->api();
 | |
| 			auto &filters = session->data().chatsFilters();
 | |
| 			const auto ids = std::make_shared<
 | |
| 				base::flat_set<mtpRequestId>
 | |
| 			>();
 | |
| 			const auto checkFinished = [=] {
 | |
| 				if (ids->empty() && next) {
 | |
| 					Assert(updated.id() != 0);
 | |
| 					next(updated);
 | |
| 				}
 | |
| 			};
 | |
| 			for (const auto &update : updates) {
 | |
| 				filters.apply(update);
 | |
| 			}
 | |
| 			auto previousId = mtpRequestId(0);
 | |
| 			const auto sendRequests = [&](const auto &requests) {
 | |
| 				for (auto &request : requests) {
 | |
| 					previousId = api->request(
 | |
| 						std::move(request)
 | |
| 					).done([=](const auto &result, mtpRequestId id) {
 | |
| 						if constexpr (std::is_same_v<
 | |
| 								std::decay_t<decltype(result)>,
 | |
| 								MTPUpdates>) {
 | |
| 							session->api().applyUpdates(result);
 | |
| 						}
 | |
| 						ids->remove(id);
 | |
| 						checkFinished();
 | |
| 					}).afterRequest(previousId).send();
 | |
| 					ids->emplace(previousId);
 | |
| 				}
 | |
| 			};
 | |
| 			sendRequests(removeRequests);
 | |
| 			sendRequests(removeChatlistRequests);
 | |
| 			sendRequests(addRequests);
 | |
| 			if (!order.empty() && !addRequests.empty()) {
 | |
| 				filters.saveOrder(order, previousId);
 | |
| 			}
 | |
| 			checkFinished();
 | |
| 			if (currentDefaultRemoved) {
 | |
| 				account->setDefaultFilterId(0);
 | |
| 				controller->setActiveChatsFilter(0);
 | |
| 			}
 | |
| 			if (localFoldersChanged) {
 | |
| 				filters.saveLocal();
 | |
| 			}
 | |
| 			if (currentDefaultId != account->defaultFilterId()) {
 | |
| 				needSave = true;
 | |
| 			}
 | |
| 			if (needSave) {
 | |
| 				Kotato::JsonSettings::Write();
 | |
| 			}
 | |
| 		});
 | |
| 	};
 | |
| 	return [copy = state->save] {
 | |
| 		copy(nullptr, nullptr);
 | |
| 	};
 | |
| }
 | |
| 
 | |
| void SetupTopContent(
 | |
| 		not_null<Ui::VerticalLayout*> parent,
 | |
| 		rpl::producer<> showFinished) {
 | |
| 	const auto divider = Ui::CreateChild<Ui::BoxContentDivider>(parent.get());
 | |
| 	const auto verticalLayout = parent->add(
 | |
| 		object_ptr<Ui::VerticalLayout>(parent.get()));
 | |
| 
 | |
| 	auto icon = CreateLottieIcon(
 | |
| 		verticalLayout,
 | |
| 		{
 | |
| 			.name = u"filters"_q,
 | |
| 			.sizeOverride = {
 | |
| 				st::settingsFilterIconSize,
 | |
| 				st::settingsFilterIconSize,
 | |
| 			},
 | |
| 		},
 | |
| 		st::settingsFilterIconPadding);
 | |
| 	std::move(
 | |
| 		showFinished
 | |
| 	) | 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<Ui::CenterWrap<>>(
 | |
| 			verticalLayout,
 | |
| 			object_ptr<Ui::FlatLabel>(
 | |
| 				verticalLayout,
 | |
| 				tr::lng_filters_about(),
 | |
| 				st::settingsFilterDividerLabel)),
 | |
| 		st::settingsFilterDividerLabelPadding);
 | |
| 
 | |
| 	verticalLayout->geometryValue(
 | |
| 	) | rpl::start_with_next([=](const QRect &r) {
 | |
| 		divider->setGeometry(r);
 | |
| 	}, divider->lifetime());
 | |
| 
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| Folders::Folders(
 | |
| 	QWidget *parent,
 | |
| 	not_null<Window::SessionController*> controller)
 | |
| : Section(parent) {
 | |
| 	setupContent(controller);
 | |
| }
 | |
| 
 | |
| Folders::~Folders() {
 | |
| 	if (!Core::Quitting()) {
 | |
| 		_save();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| rpl::producer<QString> Folders::title() {
 | |
| 	return tr::lng_filters_title();
 | |
| }
 | |
| 
 | |
| void Folders::setupContent(not_null<Window::SessionController*> controller) {
 | |
| 	controller->session().data().chatsFilters().requestSuggested();
 | |
| 
 | |
| 	const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
 | |
| 
 | |
| 	SetupTopContent(content, _showFinished.events());
 | |
| 
 | |
| 	_save = SetupFoldersContent(controller, content);
 | |
| 
 | |
| 	Ui::ResizeFitChild(this, content);
 | |
| }
 | |
| 
 | |
| void Folders::showFinished() {
 | |
| 	_showFinished.fire({});
 | |
| }
 | |
| 
 | |
| } // namespace Settings
 |