1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "boxes/connection_box.h"
9 
10 #include "ui/boxes/confirm_box.h"
11 #include "lang/lang_keys.h"
12 #include "storage/localstorage.h"
13 #include "base/qthelp_url.h"
14 #include "base/call_delayed.h"
15 #include "core/application.h"
16 #include "core/core_settings.h"
17 #include "main/main_account.h"
18 #include "mtproto/facade.h"
19 #include "ui/widgets/checkbox.h"
20 #include "ui/widgets/buttons.h"
21 #include "ui/widgets/input_fields.h"
22 #include "ui/widgets/labels.h"
23 #include "ui/widgets/dropdown_menu.h"
24 #include "ui/wrap/slide_wrap.h"
25 #include "ui/wrap/vertical_layout.h"
26 #include "ui/toast/toast.h"
27 #include "ui/effects/animations.h"
28 #include "ui/effects/radial_animation.h"
29 #include "ui/text/text_options.h"
30 #include "ui/basic_click_handlers.h"
31 #include "styles/style_layers.h"
32 #include "styles/style_boxes.h"
33 #include "styles/style_chat_helpers.h"
34 #include "styles/style_info.h"
35 
36 #include <QtGui/QGuiApplication>
37 #include <QtGui/QClipboard>
38 
39 namespace {
40 
41 constexpr auto kSaveSettingsDelayedTimeout = crl::time(1000);
42 
43 using ProxyData = MTP::ProxyData;
44 
45 class HostInput : public Ui::MaskedInputField {
46 public:
47 	HostInput(
48 		QWidget *parent,
49 		const style::InputField &st,
50 		rpl::producer<QString> placeholder,
51 		const QString &val);
52 
53 protected:
54 	void correctValue(
55 		const QString &was,
56 		int wasCursor,
57 		QString &now,
58 		int &nowCursor) override;
59 };
60 
HostInput(QWidget * parent,const style::InputField & st,rpl::producer<QString> placeholder,const QString & val)61 HostInput::HostInput(
62 	QWidget *parent,
63 	const style::InputField &st,
64 	rpl::producer<QString> placeholder,
65 	const QString &val)
66 : MaskedInputField(parent, st, std::move(placeholder), val) {
67 }
68 
correctValue(const QString & was,int wasCursor,QString & now,int & nowCursor)69 void HostInput::correctValue(
70 		const QString &was,
71 		int wasCursor,
72 		QString &now,
73 		int &nowCursor) {
74 	QString newText;
75 	int newCursor = nowCursor;
76 	newText.reserve(now.size());
77 	for (auto i = 0, l = int(now.size()); i < l; ++i) {
78 		if (now[i] == ',') {
79 			newText.append('.');
80 		} else {
81 			newText.append(now[i]);
82 		}
83 	}
84 	setCorrectedText(now, nowCursor, newText, newCursor);
85 }
86 
87 class Base64UrlInput : public Ui::MaskedInputField {
88 public:
89 	Base64UrlInput(
90 		QWidget *parent,
91 		const style::InputField &st,
92 		rpl::producer<QString> placeholder,
93 		const QString &val);
94 
95 protected:
96 	void correctValue(
97 		const QString &was,
98 		int wasCursor,
99 		QString &now,
100 		int &nowCursor) override;
101 
102 };
103 
Base64UrlInput(QWidget * parent,const style::InputField & st,rpl::producer<QString> placeholder,const QString & val)104 Base64UrlInput::Base64UrlInput(
105 	QWidget *parent,
106 	const style::InputField &st,
107 	rpl::producer<QString> placeholder,
108 	const QString &val)
109 : MaskedInputField(parent, st, std::move(placeholder), val) {
110 	if (!QRegularExpression("^[a-zA-Z0-9_\\-]+$").match(val).hasMatch()) {
111 		setText(QString());
112 	}
113 }
114 
correctValue(const QString & was,int wasCursor,QString & now,int & nowCursor)115 void Base64UrlInput::correctValue(
116 		const QString &was,
117 		int wasCursor,
118 		QString &now,
119 		int &nowCursor) {
120 	QString newText;
121 	newText.reserve(now.size());
122 	auto newPos = nowCursor;
123 	for (auto i = 0, l = int(now.size()); i < l; ++i) {
124 		const auto ch = now[i];
125 		if ((ch >= '0' && ch <= '9')
126 			|| (ch >= 'a' && ch <= 'z')
127 			|| (ch >= 'A' && ch <= 'Z')
128 			|| (ch == '-')
129 			|| (ch == '_')) {
130 			newText.append(ch);
131 		} else if (i < nowCursor) {
132 			--newPos;
133 		}
134 	}
135 	setCorrectedText(now, nowCursor, newText, newPos);
136 }
137 
138 class ProxyRow : public Ui::RippleButton {
139 public:
140 	using View = ProxiesBoxController::ItemView;
141 	using State = ProxiesBoxController::ItemState;
142 
143 	ProxyRow(QWidget *parent, View &&view);
144 
145 	void updateFields(View &&view);
146 
147 	rpl::producer<> deleteClicks() const;
148 	rpl::producer<> restoreClicks() const;
149 	rpl::producer<> editClicks() const;
150 	rpl::producer<> shareClicks() const;
151 
152 protected:
153 	int resizeGetHeight(int newWidth) override;
154 
155 	void paintEvent(QPaintEvent *e) override;
156 
157 private:
158 	void setupControls(View &&view);
159 	int countAvailableWidth() const;
160 	void radialAnimationCallback();
161 	void paintCheck(Painter &p);
162 	void showMenu();
163 
164 	View _view;
165 
166 	Ui::Text::String _title;
167 	object_ptr<Ui::IconButton> _menuToggle;
168 	rpl::event_stream<> _deleteClicks;
169 	rpl::event_stream<> _restoreClicks;
170 	rpl::event_stream<> _editClicks;
171 	rpl::event_stream<> _shareClicks;
172 	base::unique_qptr<Ui::DropdownMenu> _menu;
173 
174 	bool _set = false;
175 	Ui::Animations::Simple _toggled;
176 	Ui::Animations::Simple _setAnimation;
177 	std::unique_ptr<Ui::InfiniteRadialAnimation> _progress;
178 	std::unique_ptr<Ui::InfiniteRadialAnimation> _checking;
179 
180 	int _skipLeft = 0;
181 	int _skipRight = 0;
182 
183 };
184 
185 class ProxiesBox : public Ui::BoxContent {
186 public:
187 	using View = ProxiesBoxController::ItemView;
188 
189 	ProxiesBox(
190 		QWidget*,
191 		not_null<ProxiesBoxController*> controller,
192 		Core::SettingsProxy &settings);
193 
194 protected:
195 	void prepare() override;
196 
197 private:
198 	void setupContent();
199 	void createNoRowsLabel();
200 	void addNewProxy();
201 	void applyView(View &&view);
202 	void setupButtons(int id, not_null<ProxyRow*> button);
203 	int rowHeight() const;
204 	void refreshProxyForCalls();
205 
206 	not_null<ProxiesBoxController*> _controller;
207 	Core::SettingsProxy &_settings;
208 	QPointer<Ui::Checkbox> _tryIPv6;
209 	std::shared_ptr<Ui::RadioenumGroup<ProxyData::Settings>> _proxySettings;
210 	QPointer<Ui::SlideWrap<Ui::Checkbox>> _proxyForCalls;
211 	QPointer<Ui::DividerLabel> _about;
212 	base::unique_qptr<Ui::RpWidget> _noRows;
213 	object_ptr<Ui::VerticalLayout> _initialWrap;
214 	QPointer<Ui::VerticalLayout> _wrap;
215 	int _currentProxySupportsCallsId = 0;
216 
217 	base::flat_map<int, base::unique_qptr<ProxyRow>> _rows;
218 
219 };
220 
221 class ProxyBox final : public Ui::BoxContent {
222 public:
223 	ProxyBox(
224 		QWidget*,
225 		const ProxyData &data,
226 		Fn<void(ProxyData)> callback,
227 		Fn<void(ProxyData)> shareCallback);
228 
229 private:
230 	using Type = ProxyData::Type;
231 
232 	void prepare() override;
setInnerFocus()233 	void setInnerFocus() override {
234 		_host->setFocusFast();
235 	}
236 
237 	void refreshButtons();
238 	ProxyData collectData();
239 	void save();
240 	void share();
241 	void setupControls(const ProxyData &data);
242 	void setupTypes();
243 	void setupSocketAddress(const ProxyData &data);
244 	void setupCredentials(const ProxyData &data);
245 	void setupMtprotoCredentials(const ProxyData &data);
246 
247 	void addLabel(
248 		not_null<Ui::VerticalLayout*> parent,
249 		const QString &text) const;
250 
251 	Fn<void(ProxyData)> _callback;
252 	Fn<void(ProxyData)> _shareCallback;
253 
254 	object_ptr<Ui::VerticalLayout> _content;
255 
256 	std::shared_ptr<Ui::RadioenumGroup<Type>> _type;
257 
258 	QPointer<Ui::SlideWrap<>> _aboutSponsored;
259 	QPointer<HostInput> _host;
260 	QPointer<Ui::NumberInput> _port;
261 	QPointer<Ui::InputField> _user;
262 	QPointer<Ui::PasswordInput> _password;
263 	QPointer<Base64UrlInput> _secret;
264 
265 	QPointer<Ui::SlideWrap<Ui::VerticalLayout>> _credentials;
266 	QPointer<Ui::SlideWrap<Ui::VerticalLayout>> _mtprotoCredentials;
267 
268 };
269 
ProxyRow(QWidget * parent,View && view)270 ProxyRow::ProxyRow(QWidget *parent, View &&view)
271 : RippleButton(parent, st::proxyRowRipple)
272 , _menuToggle(this, st::topBarMenuToggle) {
273 	setupControls(std::move(view));
274 }
275 
deleteClicks() const276 rpl::producer<> ProxyRow::deleteClicks() const {
277 	return _deleteClicks.events();
278 }
279 
restoreClicks() const280 rpl::producer<> ProxyRow::restoreClicks() const {
281 	return _restoreClicks.events();
282 }
283 
editClicks() const284 rpl::producer<> ProxyRow::editClicks() const {
285 	return _editClicks.events();
286 }
287 
shareClicks() const288 rpl::producer<> ProxyRow::shareClicks() const {
289 	return _shareClicks.events();
290 }
291 
setupControls(View && view)292 void ProxyRow::setupControls(View &&view) {
293 	updateFields(std::move(view));
294 	_toggled.stop();
295 	_setAnimation.stop();
296 
297 	_menuToggle->addClickHandler([=] { showMenu(); });
298 }
299 
countAvailableWidth() const300 int ProxyRow::countAvailableWidth() const {
301 	return width() - _skipLeft - _skipRight;
302 }
303 
updateFields(View && view)304 void ProxyRow::updateFields(View &&view) {
305 	if (_view.selected != view.selected) {
306 		_toggled.start(
307 			[=] { update(); },
308 			view.selected ? 0. : 1.,
309 			view.selected ? 1. : 0.,
310 			st::defaultRadio.duration);
311 	}
312 	_view = std::move(view);
313 	const auto endpoint = _view.host + ':' + QString::number(_view.port);
314 	_title.setText(
315 		st::proxyRowTitleStyle,
316 		_view.type + ' ' + textcmdLink(1, endpoint),
317 		Ui::ItemTextDefaultOptions());
318 
319 	const auto state = _view.state;
320 	if (state == State::Connecting) {
321 		if (!_progress) {
322 			_progress = std::make_unique<Ui::InfiniteRadialAnimation>(
323 				[=] { radialAnimationCallback(); },
324 				st::proxyCheckingAnimation);
325 		}
326 		_progress->start();
327 	} else if (_progress) {
328 		_progress->stop();
329 	}
330 	if (state == State::Checking) {
331 		if (!_checking) {
332 			_checking = std::make_unique<Ui::InfiniteRadialAnimation>(
333 				[=] { radialAnimationCallback(); },
334 				st::proxyCheckingAnimation);
335 			_checking->start();
336 		}
337 	} else {
338 		_checking = nullptr;
339 	}
340 	const auto set = (state == State::Connecting || state == State::Online);
341 	if (_set != set) {
342 		_set = set;
343 		_setAnimation.start(
344 			[=] { update(); },
345 			_set ? 0. : 1.,
346 			_set ? 1. : 0.,
347 			st::defaultRadio.duration);
348 	}
349 
350 	setPointerCursor(!_view.deleted);
351 
352 	update();
353 }
354 
radialAnimationCallback()355 void ProxyRow::radialAnimationCallback() {
356 	if (!anim::Disabled()) {
357 		update();
358 	}
359 }
360 
resizeGetHeight(int newWidth)361 int ProxyRow::resizeGetHeight(int newWidth) {
362 	const auto result = st::proxyRowPadding.top()
363 		+ st::semiboldFont->height
364 		+ st::proxyRowSkip
365 		+ st::normalFont->height
366 		+ st::proxyRowPadding.bottom();
367 	auto right = st::proxyRowPadding.right();
368 	_menuToggle->moveToRight(
369 		right,
370 		(result - _menuToggle->height()) / 2,
371 		newWidth);
372 	right += _menuToggle->width();
373 	_skipRight = right;
374 	_skipLeft = st::proxyRowPadding.left()
375 		+ st::proxyRowIconSkip;
376 	return result;
377 }
378 
paintEvent(QPaintEvent * e)379 void ProxyRow::paintEvent(QPaintEvent *e) {
380 	Painter p(this);
381 
382 	if (!_view.deleted) {
383 		paintRipple(p, 0, 0);
384 	}
385 
386 	const auto left = _skipLeft;
387 	const auto availableWidth = countAvailableWidth();
388 	auto top = st::proxyRowPadding.top();
389 
390 	if (_view.deleted) {
391 		p.setOpacity(st::stickersRowDisabledOpacity);
392 	}
393 
394 	paintCheck(p);
395 
396 	p.setPen(st::proxyRowTitleFg);
397 	p.setFont(st::semiboldFont);
398 	p.setTextPalette(st::proxyRowTitlePalette);
399 	_title.drawLeftElided(p, left, top, availableWidth, width());
400 	top += st::semiboldFont->height + st::proxyRowSkip;
401 
402 	const auto statusFg = [&] {
403 		switch (_view.state) {
404 		case State::Online:
405 			return st::proxyRowStatusFgOnline;
406 		case State::Unavailable:
407 			return st::proxyRowStatusFgOffline;
408 		case State::Available:
409 			return st::proxyRowStatusFgAvailable;
410 		default:
411 			return st::proxyRowStatusFg;
412 		}
413 	}();
414 	const auto status = [&] {
415 		switch (_view.state) {
416 		case State::Available:
417 			return tr::lng_proxy_available(
418 				tr::now,
419 				lt_ping,
420 				QString::number(_view.ping));
421 		case State::Checking:
422 			return tr::lng_proxy_checking(tr::now);
423 		case State::Connecting:
424 			return tr::lng_proxy_connecting(tr::now);
425 		case State::Online:
426 			return tr::lng_proxy_online(tr::now);
427 		case State::Unavailable:
428 			return tr::lng_proxy_unavailable(tr::now);
429 		}
430 		Unexpected("State in ProxyRow::paintEvent.");
431 	}();
432 	p.setPen(_view.deleted ? st::proxyRowStatusFg : statusFg);
433 	p.setFont(st::normalFont);
434 
435 	auto statusLeft = left;
436 	if (_checking) {
437 		_checking->draw(
438 			p,
439 			{
440 				st::proxyCheckingPosition.x() + statusLeft,
441 				st::proxyCheckingPosition.y() + top
442 			},
443 			width());
444 		statusLeft += st::proxyCheckingPosition.x()
445 			+ st::proxyCheckingAnimation.size.width()
446 			+ st::proxyCheckingSkip;
447 	}
448 	p.drawTextLeft(statusLeft, top, width(), status);
449 	top += st::normalFont->height + st::proxyRowPadding.bottom();
450 }
451 
paintCheck(Painter & p)452 void ProxyRow::paintCheck(Painter &p) {
453 	const auto loading = _progress
454 		? _progress->computeState()
455 		: Ui::RadialState{ 0., 0, FullArcLength };
456 	const auto toggled = _toggled.value(_view.selected ? 1. : 0.)
457 		* (1. - loading.shown);
458 	const auto _st = &st::defaultRadio;
459 	const auto set = _setAnimation.value(_set ? 1. : 0.);
460 
461 	PainterHighQualityEnabler hq(p);
462 
463 	const auto left = st::proxyRowPadding.left();
464 	const auto top = (height() - _st->diameter - _st->thickness) / 2;
465 	const auto outerWidth = width();
466 
467 	auto pen = anim::pen(_st->untoggledFg, _st->toggledFg, toggled * set);
468 	pen.setWidth(_st->thickness);
469 	pen.setCapStyle(Qt::RoundCap);
470 	p.setPen(pen);
471 	p.setBrush(_st->bg);
472 	const auto rect = style::rtlrect(QRectF(left, top, _st->diameter, _st->diameter).marginsRemoved(QMarginsF(_st->thickness / 2., _st->thickness / 2., _st->thickness / 2., _st->thickness / 2.)), outerWidth);
473 	if (_progress && loading.shown > 0 && anim::Disabled()) {
474 		anim::DrawStaticLoading(
475 			p,
476 			rect,
477 			_st->thickness,
478 			pen.color(),
479 			_st->bg);
480 	} else if (loading.arcLength < FullArcLength) {
481 		p.drawArc(rect, loading.arcFrom, loading.arcLength);
482 	} else {
483 		p.drawEllipse(rect);
484 	}
485 
486 	if (toggled > 0 && (!_progress || !anim::Disabled())) {
487 		p.setPen(Qt::NoPen);
488 		p.setBrush(anim::brush(_st->untoggledFg, _st->toggledFg, toggled * set));
489 
490 		auto skip0 = _st->diameter / 2., skip1 = _st->skip / 10., checkSkip = skip0 * (1. - toggled) + skip1 * toggled;
491 		p.drawEllipse(style::rtlrect(QRectF(left, top, _st->diameter, _st->diameter).marginsRemoved(QMarginsF(checkSkip, checkSkip, checkSkip, checkSkip)), outerWidth));
492 	}
493 }
494 
showMenu()495 void ProxyRow::showMenu() {
496 	if (_menu) {
497 		return;
498 	}
499 	_menu = base::make_unique_q<Ui::DropdownMenu>(window());
500 	const auto weak = _menu.get();
501 	_menu->setHiddenCallback([=] {
502 		weak->deleteLater();
503 		if (_menu == weak) {
504 			_menuToggle->setForceRippled(false);
505 		}
506 	});
507 	_menu->setShowStartCallback([=] {
508 		if (_menu == weak) {
509 			_menuToggle->setForceRippled(true);
510 		}
511 	});
512 	_menu->setHideStartCallback([=] {
513 		if (_menu == weak) {
514 			_menuToggle->setForceRippled(false);
515 		}
516 	});
517 	_menuToggle->installEventFilter(_menu);
518 	const auto addAction = [&](
519 			const QString &text,
520 			Fn<void()> callback) {
521 		return _menu->addAction(text, std::move(callback));
522 	};
523 	addAction(tr::lng_proxy_menu_edit(tr::now), [=] {
524 		_editClicks.fire({});
525 	});
526 	if (_view.supportsShare) {
527 		addAction(tr::lng_proxy_edit_share(tr::now), [=] {
528 			_shareClicks.fire({});
529 		});
530 	}
531 	if (_view.deleted) {
532 		addAction(tr::lng_proxy_menu_restore(tr::now), [=] {
533 			_restoreClicks.fire({});
534 		});
535 	} else {
536 		addAction(tr::lng_proxy_menu_delete(tr::now), [=] {
537 			_deleteClicks.fire({});
538 		});
539 	}
540 	const auto parentTopLeft = window()->mapToGlobal(QPoint());
541 	const auto buttonTopLeft = _menuToggle->mapToGlobal(QPoint());
542 	const auto parent = QRect(parentTopLeft, window()->size());
543 	const auto button = QRect(buttonTopLeft, _menuToggle->size());
544 	const auto bottom = button.y()
545 		+ st::proxyDropdownDownPosition.y()
546 		+ _menu->height()
547 		- parent.y();
548 	const auto top = button.y()
549 		+ st::proxyDropdownUpPosition.y()
550 		- _menu->height()
551 		- parent.y();
552 	if (bottom > parent.height() && top >= 0) {
553 		const auto left = button.x()
554 			+ button.width()
555 			+ st::proxyDropdownUpPosition.x()
556 			- _menu->width()
557 			- parent.x();
558 		_menu->move(left, top);
559 		_menu->showAnimated(Ui::PanelAnimation::Origin::BottomRight);
560 	} else {
561 		const auto left = button.x()
562 			+ button.width()
563 			+ st::proxyDropdownDownPosition.x()
564 			- _menu->width()
565 			- parent.x();
566 		_menu->move(left, bottom - _menu->height());
567 		_menu->showAnimated(Ui::PanelAnimation::Origin::TopRight);
568 	}
569 }
570 
ProxiesBox(QWidget *,not_null<ProxiesBoxController * > controller,Core::SettingsProxy & settings)571 ProxiesBox::ProxiesBox(
572 	QWidget*,
573 	not_null<ProxiesBoxController*> controller,
574 	Core::SettingsProxy &settings)
575 : _controller(controller)
576 , _settings(settings)
577 , _initialWrap(this) {
578 	_controller->views(
579 	) | rpl::start_with_next([=](View &&view) {
580 		applyView(std::move(view));
581 	}, lifetime());
582 }
583 
prepare()584 void ProxiesBox::prepare() {
585 	setTitle(tr::lng_proxy_settings());
586 
587 	addButton(tr::lng_proxy_add(), [=] { addNewProxy(); });
588 	addButton(tr::lng_close(), [=] { closeBox(); });
589 
590 	setupContent();
591 }
592 
setupContent()593 void ProxiesBox::setupContent() {
594 	const auto inner = setInnerWidget(object_ptr<Ui::VerticalLayout>(this));
595 
596 	_tryIPv6 = inner->add(
597 		object_ptr<Ui::Checkbox>(
598 			inner,
599 			tr::lng_connection_try_ipv6(tr::now),
600 			_settings.tryIPv6()),
601 		st::proxyTryIPv6Padding);
602 	_proxySettings
603 		= std::make_shared<Ui::RadioenumGroup<ProxyData::Settings>>(
604 			_settings.settings());
605 	inner->add(
606 		object_ptr<Ui::Radioenum<ProxyData::Settings>>(
607 			inner,
608 			_proxySettings,
609 			ProxyData::Settings::Disabled,
610 			tr::lng_proxy_disable(tr::now)),
611 		st::proxyUsePadding);
612 	inner->add(
613 		object_ptr<Ui::Radioenum<ProxyData::Settings>>(
614 			inner,
615 			_proxySettings,
616 			ProxyData::Settings::System,
617 			tr::lng_proxy_use_system_settings(tr::now)),
618 		st::proxyUsePadding);
619 	inner->add(
620 		object_ptr<Ui::Radioenum<ProxyData::Settings>>(
621 			inner,
622 			_proxySettings,
623 			ProxyData::Settings::Enabled,
624 			tr::lng_proxy_use_custom(tr::now)),
625 		st::proxyUsePadding);
626 	_proxyForCalls = inner->add(
627 		object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
628 			inner,
629 			object_ptr<Ui::Checkbox>(
630 				inner,
631 				tr::lng_proxy_use_for_calls(tr::now),
632 				_settings.useProxyForCalls()),
633 			style::margins(
634 				0,
635 				st::proxyUsePadding.top(),
636 				0,
637 				st::proxyUsePadding.bottom())),
638 		style::margins(
639 			st::proxyTryIPv6Padding.left(),
640 			0,
641 			st::proxyTryIPv6Padding.right(),
642 			st::proxyTryIPv6Padding.top()));
643 
644 	_about = inner->add(
645 		object_ptr<Ui::DividerLabel>(
646 			inner,
647 			object_ptr<Ui::FlatLabel>(
648 				inner,
649 				tr::lng_proxy_about(tr::now),
650 				st::boxDividerLabel),
651 			st::proxyAboutPadding),
652 		style::margins(0, 0, 0, st::proxyRowPadding.top()));
653 
654 	_wrap = inner->add(std::move(_initialWrap));
655 	inner->add(object_ptr<Ui::FixedHeightWidget>(
656 		inner,
657 		st::proxyRowPadding.bottom()));
658 
659 	_proxySettings->setChangedCallback([=](ProxyData::Settings value) {
660 		if (!_controller->setProxySettings(value)) {
661 			_proxySettings->setValue(_settings.settings());
662 			addNewProxy();
663 		}
664 		refreshProxyForCalls();
665 	});
666 	_tryIPv6->checkedChanges(
667 	) | rpl::start_with_next([=](bool checked) {
668 		_controller->setTryIPv6(checked);
669 	}, _tryIPv6->lifetime());
670 
671 	_controller->proxySettingsValue(
672 	) | rpl::start_with_next([=](ProxyData::Settings value) {
673 		_proxySettings->setValue(value);
674 	}, inner->lifetime());
675 
676 	_proxyForCalls->entity()->checkedChanges(
677 	) | rpl::start_with_next([=](bool checked) {
678 		_controller->setProxyForCalls(checked);
679 	}, _proxyForCalls->lifetime());
680 
681 	if (_rows.empty()) {
682 		createNoRowsLabel();
683 	}
684 	refreshProxyForCalls();
685 	_proxyForCalls->finishAnimating();
686 
687 	inner->resizeToWidth(st::boxWideWidth);
688 
689 	inner->heightValue(
690 	) | rpl::map([=](int height) {
691 		return std::min(
692 			std::max(height, _about->y()
693 				+ _about->height()
694 				+ 3 * rowHeight()),
695 			st::boxMaxListHeight);
696 	}) | rpl::distinct_until_changed(
697 	) | rpl::start_with_next([=](int height) {
698 		setDimensions(st::boxWideWidth, height);
699 	}, inner->lifetime());
700 }
701 
refreshProxyForCalls()702 void ProxiesBox::refreshProxyForCalls() {
703 	if (!_proxyForCalls) {
704 		return;
705 	}
706 	_proxyForCalls->toggle(
707 		(_proxySettings->value() == ProxyData::Settings::Enabled
708 			&& _currentProxySupportsCallsId != 0),
709 		anim::type::normal);
710 }
711 
rowHeight() const712 int ProxiesBox::rowHeight() const {
713 	return st::proxyRowPadding.top()
714 		+ st::semiboldFont->height
715 		+ st::proxyRowSkip
716 		+ st::normalFont->height
717 		+ st::proxyRowPadding.bottom();
718 }
719 
addNewProxy()720 void ProxiesBox::addNewProxy() {
721 	getDelegate()->show(_controller->addNewItemBox());
722 }
723 
applyView(View && view)724 void ProxiesBox::applyView(View &&view) {
725 	if (view.selected) {
726 		_currentProxySupportsCallsId = view.supportsCalls ? view.id : 0;
727 	} else if (view.id == _currentProxySupportsCallsId) {
728 		_currentProxySupportsCallsId = 0;
729 	}
730 	refreshProxyForCalls();
731 
732 	const auto id = view.id;
733 	const auto i = _rows.find(id);
734 	if (i == _rows.end()) {
735 		const auto wrap = _wrap
736 			? _wrap.data()
737 			: _initialWrap.data();
738 		const auto [i, ok] = _rows.emplace(id, nullptr);
739 		i->second.reset(wrap->insert(
740 			0,
741 			object_ptr<ProxyRow>(
742 				wrap,
743 				std::move(view))));
744 		setupButtons(id, i->second.get());
745 		if (_noRows) {
746 			_noRows.reset();
747 		}
748 		wrap->resizeToWidth(width());
749 	} else if (view.host.isEmpty()) {
750 		_rows.erase(i);
751 	} else {
752 		i->second->updateFields(std::move(view));
753 	}
754 }
755 
createNoRowsLabel()756 void ProxiesBox::createNoRowsLabel() {
757 	_noRows.reset(_wrap->add(
758 		object_ptr<Ui::FixedHeightWidget>(
759 			_wrap,
760 			rowHeight()),
761 		st::proxyEmptyListPadding));
762 	_noRows->resize(
763 		(st::boxWideWidth
764 			- st::proxyEmptyListPadding.left()
765 			- st::proxyEmptyListPadding.right()),
766 		_noRows->height());
767 	const auto label = Ui::CreateChild<Ui::FlatLabel>(
768 		_noRows.get(),
769 		tr::lng_proxy_description(tr::now),
770 		st::proxyEmptyListLabel);
771 	_noRows->widthValue(
772 	) | rpl::start_with_next([=](int width) {
773 		label->resizeToWidth(width);
774 		label->moveToLeft(0, 0);
775 	}, label->lifetime());
776 }
777 
setupButtons(int id,not_null<ProxyRow * > button)778 void ProxiesBox::setupButtons(int id, not_null<ProxyRow*> button) {
779 	button->deleteClicks(
780 	) | rpl::start_with_next([=] {
781 		_controller->deleteItem(id);
782 	}, button->lifetime());
783 
784 	button->restoreClicks(
785 	) | rpl::start_with_next([=] {
786 		_controller->restoreItem(id);
787 	}, button->lifetime());
788 
789 	button->editClicks(
790 	) | rpl::start_with_next([=] {
791 		getDelegate()->show(_controller->editItemBox(id));
792 	}, button->lifetime());
793 
794 	button->shareClicks(
795 	) | rpl::start_with_next([=] {
796 		_controller->shareItem(id);
797 	}, button->lifetime());
798 
799 	button->clicks(
800 	) | rpl::start_with_next([=] {
801 		_controller->applyItem(id);
802 	}, button->lifetime());
803 }
804 
ProxyBox(QWidget *,const ProxyData & data,Fn<void (ProxyData)> callback,Fn<void (ProxyData)> shareCallback)805 ProxyBox::ProxyBox(
806 	QWidget*,
807 	const ProxyData &data,
808 	Fn<void(ProxyData)> callback,
809 	Fn<void(ProxyData)> shareCallback)
810 : _callback(std::move(callback))
811 , _shareCallback(std::move(shareCallback))
812 , _content(this) {
813 	setupControls(data);
814 }
815 
prepare()816 void ProxyBox::prepare() {
817 	setTitle(tr::lng_proxy_edit());
818 
819 	connect(_host.data(), &HostInput::changed, [=] {
820 		Ui::PostponeCall(_host, [=] {
821 			const auto host = _host->getLastText().trimmed();
822 			static const auto mask = u"^\\d+\\.\\d+\\.\\d+\\.\\d+:(\\d*)$"_q;
823 			const auto match = QRegularExpression(mask).match(host);
824 			if (_host->cursorPosition() == host.size()
825 				&& match.hasMatch()) {
826 				const auto port = match.captured(1);
827 				_port->setText(port);
828 				_port->setCursorPosition(port.size());
829 				_port->setFocus();
830 				_host->setText(host.mid(0, host.size() - port.size() - 1));
831 			}
832 		});
833 	});
834 	_port.data()->events(
835 	) | rpl::start_with_next([=](not_null<QEvent*> e) {
836 		if (e->type() == QEvent::KeyPress
837 			&& (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Backspace)
838 			&& _port->cursorPosition() == 0) {
839 			_host->setCursorPosition(_host->getLastText().size());
840 			_host->setFocus();
841 		}
842 	}, _port->lifetime());
843 
844 	refreshButtons();
845 	setDimensionsToContent(st::boxWideWidth, _content);
846 }
847 
refreshButtons()848 void ProxyBox::refreshButtons() {
849 	clearButtons();
850 	addButton(tr::lng_settings_save(), [=] { save(); });
851 	addButton(tr::lng_cancel(), [=] { closeBox(); });
852 
853 	const auto type = _type->value();
854 	if (type == Type::Socks5 || type == Type::Mtproto) {
855 		addLeftButton(tr::lng_proxy_share(), [=] { share(); });
856 	}
857 }
858 
save()859 void ProxyBox::save() {
860 	if (const auto data = collectData()) {
861 		_callback(data);
862 		closeBox();
863 	}
864 }
865 
share()866 void ProxyBox::share() {
867 	if (const auto data = collectData()) {
868 		_shareCallback(data);
869 	}
870 }
871 
collectData()872 ProxyData ProxyBox::collectData() {
873 	auto result = ProxyData();
874 	result.type = _type->value();
875 	result.host = _host->getLastText().trimmed();
876 	result.port = _port->getLastText().trimmed().toInt();
877 	result.user = (result.type == Type::Mtproto)
878 		? QString()
879 		: _user->getLastText();
880 	result.password = (result.type == Type::Mtproto)
881 		? _secret->getLastText()
882 		: _password->getLastText();
883 	if (result.host.isEmpty()) {
884 		_host->showError();
885 	} else if (!result.port) {
886 		_port->showError();
887 	} else if ((result.type == Type::Http || result.type == Type::Socks5)
888 		&& !result.password.isEmpty() && result.user.isEmpty()) {
889 		_user->showError();
890 	} else if (result.type == Type::Mtproto && !result.valid()) {
891 		_secret->showError();
892 	} else if (!result) {
893 		_host->showError();
894 	} else {
895 		return result;
896 	}
897 	return ProxyData();
898 }
899 
setupTypes()900 void ProxyBox::setupTypes() {
901 	const auto types = std::map<Type, QString>{
902 		{ Type::Http, "HTTP" },
903 		{ Type::Socks5, "SOCKS5" },
904 		{ Type::Mtproto, "MTPROTO" },
905 	};
906 	for (const auto &[type, label] : types) {
907 		_content->add(
908 			object_ptr<Ui::Radioenum<Type>>(
909 				_content,
910 				_type,
911 				type,
912 				label),
913 			st::proxyEditTypePadding);
914 	}
915 	_aboutSponsored = _content->add(object_ptr<Ui::SlideWrap<>>(
916 		_content,
917 		object_ptr<Ui::PaddingWrap<>>(
918 			_content,
919 			object_ptr<Ui::FlatLabel>(
920 				_content,
921 				tr::lng_proxy_sponsor_warning(tr::now),
922 				st::boxDividerLabel),
923 			st::proxyAboutSponsorPadding)));
924 }
925 
setupSocketAddress(const ProxyData & data)926 void ProxyBox::setupSocketAddress(const ProxyData &data) {
927 	addLabel(_content, tr::lng_proxy_address_label(tr::now));
928 	const auto address = _content->add(
929 		object_ptr<Ui::FixedHeightWidget>(
930 			_content,
931 			st::connectionHostInputField.heightMin),
932 		st::proxyEditInputPadding);
933 	_host = Ui::CreateChild<HostInput>(
934 		address,
935 		st::connectionHostInputField,
936 		tr::lng_connection_host_ph(),
937 		data.host);
938 	_port = Ui::CreateChild<Ui::NumberInput>(
939 		address,
940 		st::connectionPortInputField,
941 		tr::lng_connection_port_ph(),
942 		data.port ? QString::number(data.port) : QString(),
943 		65535);
944 	address->widthValue(
945 	) | rpl::start_with_next([=](int width) {
946 		_port->moveToRight(0, 0);
947 		_host->resize(
948 			width - _port->width() - st::proxyEditSkip,
949 			_host->height());
950 		_host->moveToLeft(0, 0);
951 	}, address->lifetime());
952 }
953 
setupCredentials(const ProxyData & data)954 void ProxyBox::setupCredentials(const ProxyData &data) {
955 		_credentials = _content->add(
956 		object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
957 			_content,
958 			object_ptr<Ui::VerticalLayout>(_content)));
959 	const auto credentials = _credentials->entity();
960 	addLabel(credentials, tr::lng_proxy_credentials_optional(tr::now));
961 	_user = credentials->add(
962 		object_ptr<Ui::InputField>(
963 			credentials,
964 			st::connectionUserInputField,
965 			tr::lng_connection_user_ph(),
966 			data.user),
967 		st::proxyEditInputPadding);
968 
969 	auto passwordWrap = object_ptr<Ui::RpWidget>(credentials);
970 	_password = Ui::CreateChild<Ui::PasswordInput>(
971 		passwordWrap.data(),
972 		st::connectionPasswordInputField,
973 		tr::lng_connection_password_ph(),
974 		(data.type == Type::Mtproto) ? QString() : data.password);
975 	_password->move(0, 0);
976 	_password->heightValue(
977 	) | rpl::start_with_next([=, wrap = passwordWrap.data()](int height) {
978 		wrap->resize(wrap->width(), height);
979 	}, _password->lifetime());
980 	passwordWrap->widthValue(
981 	) | rpl::start_with_next([=](int width) {
982 		_password->resize(width, _password->height());
983 	}, _password->lifetime());
984 	credentials->add(std::move(passwordWrap), st::proxyEditInputPadding);
985 }
986 
setupMtprotoCredentials(const ProxyData & data)987 void ProxyBox::setupMtprotoCredentials(const ProxyData &data) {
988 	_mtprotoCredentials = _content->add(
989 		object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
990 			_content,
991 			object_ptr<Ui::VerticalLayout>(_content)));
992 	const auto mtproto = _mtprotoCredentials->entity();
993 	addLabel(mtproto, tr::lng_proxy_credentials(tr::now));
994 
995 	auto secretWrap = object_ptr<Ui::RpWidget>(mtproto);
996 	_secret = Ui::CreateChild<Base64UrlInput>(
997 		secretWrap.data(),
998 		st::connectionUserInputField,
999 		tr::lng_connection_proxy_secret_ph(),
1000 		(data.type == Type::Mtproto) ? data.password : QString());
1001 	_secret->move(0, 0);
1002 	_secret->heightValue(
1003 	) | rpl::start_with_next([=, wrap = secretWrap.data()](int height) {
1004 		wrap->resize(wrap->width(), height);
1005 	}, _secret->lifetime());
1006 	secretWrap->widthValue(
1007 	) | rpl::start_with_next([=](int width) {
1008 		_secret->resize(width, _secret->height());
1009 	}, _secret->lifetime());
1010 	mtproto->add(std::move(secretWrap), st::proxyEditInputPadding);
1011 }
1012 
setupControls(const ProxyData & data)1013 void ProxyBox::setupControls(const ProxyData &data) {
1014 	_type = std::make_shared<Ui::RadioenumGroup<Type>>(
1015 		(data.type == Type::None
1016 			? Type::Socks5
1017 			: data.type));
1018 	_content.create(this);
1019 	_content->resizeToWidth(st::boxWideWidth);
1020 	_content->moveToLeft(0, 0);
1021 
1022 	setupTypes();
1023 	setupSocketAddress(data);
1024 	setupCredentials(data);
1025 	setupMtprotoCredentials(data);
1026 
1027 	const auto handleType = [=](Type type) {
1028 		_credentials->toggle(
1029 			type == Type::Http || type == Type::Socks5,
1030 			anim::type::instant);
1031 		_mtprotoCredentials->toggle(
1032 			type == Type::Mtproto,
1033 			anim::type::instant);
1034 		_aboutSponsored->toggle(
1035 			type == Type::Mtproto,
1036 			anim::type::instant);
1037 	};
1038 	_type->setChangedCallback([=](Type type) {
1039 		handleType(type);
1040 		refreshButtons();
1041 	});
1042 	handleType(_type->value());
1043 }
1044 
addLabel(not_null<Ui::VerticalLayout * > parent,const QString & text) const1045 void ProxyBox::addLabel(
1046 		not_null<Ui::VerticalLayout*> parent,
1047 		const QString &text) const {
1048 	parent->add(
1049 		object_ptr<Ui::FlatLabel>(
1050 			parent,
1051 			text,
1052 			st::proxyEditTitle),
1053 		st::proxyEditTitlePadding);
1054 }
1055 
1056 } // namespace
1057 
ProxiesBoxController(not_null<Main::Account * > account)1058 ProxiesBoxController::ProxiesBoxController(not_null<Main::Account*> account)
1059 : _account(account)
1060 , _settings(Core::App().settings().proxy())
1061 , _saveTimer([] { Local::writeSettings(); }) {
1062 	_list = ranges::views::all(
1063 		_settings.list()
__anonf7c060f62e02(const ProxyData &proxy) 1064 	) | ranges::views::transform([&](const ProxyData &proxy) {
1065 		return Item{ ++_idCounter, proxy };
1066 	}) | ranges::to_vector;
1067 
1068 	_settings.connectionTypeChanges(
__anonf7c060f62f02null1069 	) | rpl::start_with_next([=] {
1070 		_proxySettingsChanges.fire_copy(_settings.settings());
1071 		const auto i = findByProxy(_settings.selected());
1072 		if (i != end(_list)) {
1073 			updateView(*i);
1074 		}
1075 	}, _lifetime);
1076 
1077 	for (auto &item : _list) {
1078 		refreshChecker(item);
1079 	}
1080 }
1081 
ShowApplyConfirmation(Type type,const QMap<QString,QString> & fields)1082 void ProxiesBoxController::ShowApplyConfirmation(
1083 		Type type,
1084 		const QMap<QString, QString> &fields) {
1085 	const auto server = fields.value(qsl("server"));
1086 	const auto port = fields.value(qsl("port")).toUInt();
1087 	auto proxy = ProxyData();
1088 	proxy.type = type;
1089 	proxy.host = server;
1090 	proxy.port = port;
1091 	if (type == Type::Socks5) {
1092 		proxy.user = fields.value(qsl("user"));
1093 		proxy.password = fields.value(qsl("pass"));
1094 	} else if (type == Type::Mtproto) {
1095 		proxy.password = fields.value(qsl("secret"));
1096 	}
1097 	if (proxy) {
1098 		const auto displayed = "https://" + server + "/";
1099 		const auto parsed = QUrl::fromUserInput(displayed);
1100 		const auto displayUrl = !UrlClickHandler::IsSuspicious(displayed)
1101 			? displayed
1102 			: parsed.isValid()
1103 			? QString::fromUtf8(parsed.toEncoded())
1104 			: UrlClickHandler::ShowEncoded(displayed);
1105 		const auto displayServer = QString(
1106 			displayUrl
1107 		).replace(
1108 			QRegularExpression(
1109 				"^https://",
1110 				QRegularExpression::CaseInsensitiveOption),
1111 			QString()
1112 		).replace(QRegularExpression("/$"), QString());
1113 		const auto text = tr::lng_sure_enable_socks(
1114 			tr::now,
1115 			lt_server,
1116 			displayServer,
1117 			lt_port,
1118 			QString::number(port))
1119 			+ (proxy.type == Type::Mtproto
1120 				? "\n\n" + tr::lng_proxy_sponsor_warning(tr::now)
1121 				: QString());
1122 		auto callback = [=](Fn<void()> &&close) {
1123 			auto &proxies = Core::App().settings().proxy().list();
1124 			if (!ranges::contains(proxies, proxy)) {
1125 				proxies.push_back(proxy);
1126 			}
1127 			Core::App().setCurrentProxy(
1128 				proxy,
1129 				ProxyData::Settings::Enabled);
1130 			Local::writeSettings();
1131 			close();
1132 		};
1133 		Ui::show(
1134 			Box<Ui::ConfirmBox>(
1135 				text,
1136 				tr::lng_sure_enable(tr::now),
1137 				std::move(callback)),
1138 			Ui::LayerOption::KeepOther);
1139 	} else {
1140 		Ui::show(Box<Ui::InformBox>(
1141 			(proxy.status() == ProxyData::Status::Unsupported
1142 				? tr::lng_proxy_unsupported(tr::now)
1143 				: tr::lng_proxy_invalid(tr::now))));
1144 	}
1145 }
1146 
proxySettingsValue() const1147 auto ProxiesBoxController::proxySettingsValue() const
1148 -> rpl::producer<ProxyData::Settings> {
1149 	return _proxySettingsChanges.events_starting_with_copy(
1150 		_settings.settings()
1151 	) | rpl::distinct_until_changed();
1152 }
1153 
refreshChecker(Item & item)1154 void ProxiesBoxController::refreshChecker(Item &item) {
1155 	using Variants = MTP::DcOptions::Variants;
1156 	const auto type = (item.data.type == Type::Http)
1157 		? Variants::Http
1158 		: Variants::Tcp;
1159 	const auto mtproto = &_account->mtp();
1160 	const auto dcId = mtproto->mainDcId();
1161 	const auto forFiles = false;
1162 
1163 	item.state = ItemState::Checking;
1164 	const auto setup = [&](Checker &checker, const bytes::vector &secret) {
1165 		checker = MTP::details::AbstractConnection::Create(
1166 			mtproto,
1167 			type,
1168 			QThread::currentThread(),
1169 			secret,
1170 			item.data);
1171 		setupChecker(item.id, checker);
1172 	};
1173 	if (item.data.type == Type::Mtproto) {
1174 		const auto secret = item.data.secretFromMtprotoPassword();
1175 		setup(item.checker, secret);
1176 		item.checker->connectToServer(
1177 			item.data.host,
1178 			item.data.port,
1179 			secret,
1180 			dcId,
1181 			forFiles);
1182 		item.checkerv6 = nullptr;
1183 	} else {
1184 		const auto options = mtproto->dcOptions().lookup(
1185 			dcId,
1186 			MTP::DcType::Regular,
1187 			true);
1188 		const auto connect = [&](
1189 				Checker &checker,
1190 				Variants::Address address) {
1191 			const auto &list = options.data[address][type];
1192 			if (list.empty()
1193 				|| ((address == Variants::IPv6)
1194 					&& !Core::App().settings().proxy().tryIPv6())) {
1195 				checker = nullptr;
1196 				return;
1197 			}
1198 			const auto &endpoint = list.front();
1199 			setup(checker, endpoint.secret);
1200 			checker->connectToServer(
1201 				QString::fromStdString(endpoint.ip),
1202 				endpoint.port,
1203 				endpoint.secret,
1204 				dcId,
1205 				forFiles);
1206 		};
1207 		connect(item.checker, Variants::IPv4);
1208 		connect(item.checkerv6, Variants::IPv6);
1209 		if (!item.checker && !item.checkerv6) {
1210 			item.state = ItemState::Unavailable;
1211 		}
1212 	}
1213 }
1214 
setupChecker(int id,const Checker & checker)1215 void ProxiesBoxController::setupChecker(int id, const Checker &checker) {
1216 	using Connection = MTP::details::AbstractConnection;
1217 	const auto pointer = checker.get();
1218 	pointer->connect(pointer, &Connection::connected, [=] {
1219 		const auto item = findById(id);
1220 		const auto pingTime = pointer->pingTime();
1221 		item->checker = nullptr;
1222 		item->checkerv6 = nullptr;
1223 		if (item->state == ItemState::Checking) {
1224 			item->state = ItemState::Available;
1225 			item->ping = pingTime;
1226 			updateView(*item);
1227 		}
1228 	});
1229 	const auto failed = [=] {
1230 		const auto item = findById(id);
1231 		if (item->checker == pointer) {
1232 			item->checker = nullptr;
1233 		} else if (item->checkerv6 == pointer) {
1234 			item->checkerv6 = nullptr;
1235 		}
1236 		if (!item->checker
1237 			&& !item->checkerv6
1238 			&& item->state == ItemState::Checking) {
1239 			item->state = ItemState::Unavailable;
1240 			updateView(*item);
1241 		}
1242 	};
1243 	pointer->connect(pointer, &Connection::disconnected, failed);
1244 	pointer->connect(pointer, &Connection::error, failed);
1245 }
1246 
CreateOwningBox(not_null<Main::Account * > account)1247 object_ptr<Ui::BoxContent> ProxiesBoxController::CreateOwningBox(
1248 		not_null<Main::Account*> account) {
1249 	auto controller = std::make_unique<ProxiesBoxController>(account);
1250 	auto box = controller->create();
1251 	Ui::AttachAsChild(box, std::move(controller));
1252 	return box;
1253 }
1254 
create()1255 object_ptr<Ui::BoxContent> ProxiesBoxController::create() {
1256 	auto result = Box<ProxiesBox>(this, _settings);
1257 	for (const auto &item : _list) {
1258 		updateView(item);
1259 	}
1260 	return result;
1261 }
1262 
findById(int id)1263 auto ProxiesBoxController::findById(int id) -> std::vector<Item>::iterator {
1264 	const auto result = ranges::find(
1265 		_list,
1266 		id,
1267 		[](const Item &item) { return item.id; });
1268 	Assert(result != end(_list));
1269 	return result;
1270 }
1271 
findByProxy(const ProxyData & proxy)1272 auto ProxiesBoxController::findByProxy(const ProxyData &proxy)
1273 ->std::vector<Item>::iterator {
1274 	return ranges::find(
1275 		_list,
1276 		proxy,
1277 		[](const Item &item) { return item.data; });
1278 }
1279 
deleteItem(int id)1280 void ProxiesBoxController::deleteItem(int id) {
1281 	setDeleted(id, true);
1282 }
1283 
restoreItem(int id)1284 void ProxiesBoxController::restoreItem(int id) {
1285 	setDeleted(id, false);
1286 }
1287 
shareItem(int id)1288 void ProxiesBoxController::shareItem(int id) {
1289 	share(findById(id)->data);
1290 }
1291 
applyItem(int id)1292 void ProxiesBoxController::applyItem(int id) {
1293 	auto item = findById(id);
1294 	if (_settings.isEnabled() && (_settings.selected() == item->data)) {
1295 		return;
1296 	} else if (item->deleted) {
1297 		return;
1298 	}
1299 
1300 	auto j = findByProxy(_settings.selected());
1301 
1302 	Core::App().setCurrentProxy(
1303 		item->data,
1304 		ProxyData::Settings::Enabled);
1305 	saveDelayed();
1306 
1307 	if (j != end(_list)) {
1308 		updateView(*j);
1309 	}
1310 	updateView(*item);
1311 }
1312 
setDeleted(int id,bool deleted)1313 void ProxiesBoxController::setDeleted(int id, bool deleted) {
1314 	auto item = findById(id);
1315 	item->deleted = deleted;
1316 
1317 	if (deleted) {
1318 		auto &proxies = _settings.list();
1319 		proxies.erase(ranges::remove(proxies, item->data), end(proxies));
1320 
1321 		if (item->data == _settings.selected()) {
1322 			_lastSelectedProxy = _settings.selected();
1323 			_settings.setSelected(MTP::ProxyData());
1324 			if (_settings.isEnabled()) {
1325 				_lastSelectedProxyUsed = true;
1326 				Core::App().setCurrentProxy(
1327 					ProxyData(),
1328 					ProxyData::Settings::System);
1329 				saveDelayed();
1330 			} else {
1331 				_lastSelectedProxyUsed = false;
1332 			}
1333 		}
1334 	} else {
1335 		auto &proxies = _settings.list();
1336 		if (ranges::find(proxies, item->data) == end(proxies)) {
1337 			auto insertBefore = item + 1;
1338 			while (insertBefore != end(_list) && insertBefore->deleted) {
1339 				++insertBefore;
1340 			}
1341 			auto insertBeforeIt = (insertBefore == end(_list))
1342 				? end(proxies)
1343 				: ranges::find(proxies, insertBefore->data);
1344 			proxies.insert(insertBeforeIt, item->data);
1345 		}
1346 
1347 		if (!_settings.selected() && _lastSelectedProxy == item->data) {
1348 			Assert(!_settings.isEnabled());
1349 
1350 			if (base::take(_lastSelectedProxyUsed)) {
1351 				Core::App().setCurrentProxy(
1352 					base::take(_lastSelectedProxy),
1353 					ProxyData::Settings::Enabled);
1354 			} else {
1355 				_settings.setSelected(base::take(_lastSelectedProxy));
1356 			}
1357 		}
1358 	}
1359 	saveDelayed();
1360 	updateView(*item);
1361 }
1362 
editItemBox(int id)1363 object_ptr<Ui::BoxContent> ProxiesBoxController::editItemBox(int id) {
1364 	return Box<ProxyBox>(findById(id)->data, [=](const ProxyData &result) {
1365 		auto i = findById(id);
1366 		auto j = ranges::find(
1367 			_list,
1368 			result,
1369 			[](const Item &item) { return item.data; });
1370 		if (j != end(_list) && j != i) {
1371 			replaceItemWith(i, j);
1372 		} else {
1373 			replaceItemValue(i, result);
1374 		}
1375 	}, [=](const ProxyData &proxy) {
1376 		share(proxy);
1377 	});
1378 }
1379 
replaceItemWith(std::vector<Item>::iterator which,std::vector<Item>::iterator with)1380 void ProxiesBoxController::replaceItemWith(
1381 		std::vector<Item>::iterator which,
1382 		std::vector<Item>::iterator with) {
1383 	auto &proxies = _settings.list();
1384 	proxies.erase(ranges::remove(proxies, which->data), end(proxies));
1385 
1386 	_views.fire({ which->id });
1387 	_list.erase(which);
1388 
1389 	if (with->deleted) {
1390 		restoreItem(with->id);
1391 	}
1392 	applyItem(with->id);
1393 	saveDelayed();
1394 }
1395 
replaceItemValue(std::vector<Item>::iterator which,const ProxyData & proxy)1396 void ProxiesBoxController::replaceItemValue(
1397 		std::vector<Item>::iterator which,
1398 		const ProxyData &proxy) {
1399 	if (which->deleted) {
1400 		restoreItem(which->id);
1401 	}
1402 
1403 	auto &proxies = _settings.list();
1404 	const auto i = ranges::find(proxies, which->data);
1405 	Assert(i != end(proxies));
1406 	*i = proxy;
1407 	which->data = proxy;
1408 	refreshChecker(*which);
1409 
1410 	applyItem(which->id);
1411 	saveDelayed();
1412 }
1413 
addNewItemBox()1414 object_ptr<Ui::BoxContent> ProxiesBoxController::addNewItemBox() {
1415 	return Box<ProxyBox>(ProxyData(), [=](const ProxyData &result) {
1416 		auto j = ranges::find(
1417 			_list,
1418 			result,
1419 			[](const Item &item) { return item.data; });
1420 		if (j != end(_list)) {
1421 			if (j->deleted) {
1422 				restoreItem(j->id);
1423 			}
1424 			applyItem(j->id);
1425 		} else {
1426 			addNewItem(result);
1427 		}
1428 	}, [=](const ProxyData &proxy) {
1429 		share(proxy);
1430 	});
1431 }
1432 
addNewItem(const ProxyData & proxy)1433 void ProxiesBoxController::addNewItem(const ProxyData &proxy) {
1434 	auto &proxies = _settings.list();
1435 	proxies.push_back(proxy);
1436 
1437 	_list.push_back({ ++_idCounter, proxy });
1438 	refreshChecker(_list.back());
1439 	applyItem(_list.back().id);
1440 }
1441 
setProxySettings(ProxyData::Settings value)1442 bool ProxiesBoxController::setProxySettings(ProxyData::Settings value) {
1443 	if (_settings.settings() == value) {
1444 		return true;
1445 	} else if (value == ProxyData::Settings::Enabled) {
1446 		if (_settings.list().empty()) {
1447 			return false;
1448 		} else if (!_settings.selected()) {
1449 			_settings.setSelected(_settings.list().back());
1450 			auto j = findByProxy(_settings.selected());
1451 			if (j != end(_list)) {
1452 				updateView(*j);
1453 			}
1454 		}
1455 	}
1456 	Core::App().setCurrentProxy(_settings.selected(), value);
1457 	saveDelayed();
1458 	return true;
1459 }
1460 
setProxyForCalls(bool enabled)1461 void ProxiesBoxController::setProxyForCalls(bool enabled) {
1462 	if (_settings.useProxyForCalls() == enabled) {
1463 		return;
1464 	}
1465 	_settings.setUseProxyForCalls(enabled);
1466 	if (_settings.isEnabled() && _settings.selected().supportsCalls()) {
1467 		_settings.connectionTypeChangesNotify();
1468 	}
1469 	saveDelayed();
1470 }
1471 
setTryIPv6(bool enabled)1472 void ProxiesBoxController::setTryIPv6(bool enabled) {
1473 	if (Core::App().settings().proxy().tryIPv6() == enabled) {
1474 		return;
1475 	}
1476 	Core::App().settings().proxy().setTryIPv6(enabled);
1477 	_account->mtp().restart();
1478 	_settings.connectionTypeChangesNotify();
1479 	saveDelayed();
1480 }
1481 
saveDelayed()1482 void ProxiesBoxController::saveDelayed() {
1483 	_saveTimer.callOnce(kSaveSettingsDelayedTimeout);
1484 }
1485 
views() const1486 auto ProxiesBoxController::views() const -> rpl::producer<ItemView> {
1487 	return _views.events();
1488 }
1489 
updateView(const Item & item)1490 void ProxiesBoxController::updateView(const Item &item) {
1491 	const auto selected = (_settings.selected() == item.data);
1492 	const auto deleted = item.deleted;
1493 	const auto type = [&] {
1494 		switch (item.data.type) {
1495 		case Type::Http:
1496 			return qsl("HTTP");
1497 		case Type::Socks5:
1498 			return qsl("SOCKS5");
1499 		case Type::Mtproto:
1500 			return qsl("MTPROTO");
1501 		}
1502 		Unexpected("Proxy type in ProxiesBoxController::updateView.");
1503 	}();
1504 	const auto state = [&] {
1505 		if (!selected || !_settings.isEnabled()) {
1506 			return item.state;
1507 		} else if (_account->mtp().dcstate() == MTP::ConnectedState) {
1508 			return ItemState::Online;
1509 		}
1510 		return ItemState::Connecting;
1511 	}();
1512 	const auto supportsShare = (item.data.type == Type::Socks5)
1513 		|| (item.data.type == Type::Mtproto);
1514 	const auto supportsCalls = item.data.supportsCalls();
1515 	_views.fire({
1516 		item.id,
1517 		type,
1518 		item.data.host,
1519 		item.data.port,
1520 		item.ping,
1521 		!deleted && selected,
1522 		deleted,
1523 		!deleted && supportsShare,
1524 		supportsCalls,
1525 		state });
1526 }
1527 
share(const ProxyData & proxy)1528 void ProxiesBoxController::share(const ProxyData &proxy) {
1529 	if (proxy.type == Type::Http) {
1530 		return;
1531 	}
1532 	const auto link = qsl("https://t.me/")
1533 		+ (proxy.type == Type::Socks5 ? "socks" : "proxy")
1534 		+ "?server=" + proxy.host + "&port=" + QString::number(proxy.port)
1535 		+ ((proxy.type == Type::Socks5 && !proxy.user.isEmpty())
1536 			? "&user=" + qthelp::url_encode(proxy.user) : "")
1537 		+ ((proxy.type == Type::Socks5 && !proxy.password.isEmpty())
1538 			? "&pass=" + qthelp::url_encode(proxy.password) : "")
1539 		+ ((proxy.type == Type::Mtproto && !proxy.password.isEmpty())
1540 			? "&secret=" + proxy.password : "");
1541 	QGuiApplication::clipboard()->setText(link);
1542 	Ui::Toast::Show(tr::lng_username_copied(tr::now));
1543 }
1544 
~ProxiesBoxController()1545 ProxiesBoxController::~ProxiesBoxController() {
1546 	if (_saveTimer.isActive()) {
1547 		base::call_delayed(
1548 			kSaveSettingsDelayedTimeout,
1549 			QCoreApplication::instance(),
1550 			[] { Local::writeSettings(); });
1551 	}
1552 }
1553