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/create_poll_box.h"
9 
10 #include "lang/lang_keys.h"
11 #include "data/data_poll.h"
12 #include "ui/toast/toast.h"
13 #include "ui/wrap/vertical_layout.h"
14 #include "ui/wrap/slide_wrap.h"
15 #include "ui/wrap/fade_wrap.h"
16 #include "ui/widgets/input_fields.h"
17 #include "ui/widgets/shadow.h"
18 #include "ui/widgets/labels.h"
19 #include "ui/widgets/buttons.h"
20 #include "ui/widgets/checkbox.h"
21 #include "ui/toast/toast.h"
22 #include "main/main_session.h"
23 #include "core/application.h"
24 #include "core/core_settings.h"
25 #include "chat_helpers/emoji_suggestions_widget.h"
26 #include "chat_helpers/message_field.h"
27 #include "chat_helpers/send_context_menu.h"
28 #include "history/view/history_view_schedule_box.h"
29 #include "settings/settings_common.h"
30 #include "base/unique_qptr.h"
31 #include "base/event_filter.h"
32 #include "base/call_delayed.h"
33 #include "base/random.h"
34 #include "window/window_session_controller.h"
35 #include "styles/style_layers.h"
36 #include "styles/style_boxes.h"
37 #include "styles/style_settings.h"
38 
39 namespace {
40 
41 constexpr auto kQuestionLimit = 255;
42 constexpr auto kMaxOptionsCount = PollData::kMaxOptions;
43 constexpr auto kOptionLimit = 100;
44 constexpr auto kWarnQuestionLimit = 80;
45 constexpr auto kWarnOptionLimit = 30;
46 constexpr auto kSolutionLimit = 200;
47 constexpr auto kWarnSolutionLimit = 60;
48 constexpr auto kErrorLimit = 99;
49 
50 class Options {
51 public:
52 	Options(
53 		not_null<QWidget*> outer,
54 		not_null<Ui::VerticalLayout*> container,
55 		not_null<Main::Session*> session,
56 		bool chooseCorrectEnabled);
57 
58 	[[nodiscard]] bool hasOptions() const;
59 	[[nodiscard]] bool isValid() const;
60 	[[nodiscard]] bool hasCorrect() const;
61 	[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
62 	void focusFirst();
63 
64 	void enableChooseCorrect(bool enabled);
65 
66 	[[nodiscard]] rpl::producer<int> usedCount() const;
67 	[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
68 	[[nodiscard]] rpl::producer<> backspaceInFront() const;
69 	[[nodiscard]] rpl::producer<> tabbed() const;
70 
71 private:
72 	class Option {
73 	public:
74 		Option(
75 			not_null<QWidget*> outer,
76 			not_null<Ui::VerticalLayout*> container,
77 			not_null<Main::Session*> session,
78 			int position,
79 			std::shared_ptr<Ui::RadiobuttonGroup> group);
80 
81 		Option(const Option &other) = delete;
82 		Option &operator=(const Option &other) = delete;
83 
84 		void toggleRemoveAlways(bool toggled);
85 		void enableChooseCorrect(
86 			std::shared_ptr<Ui::RadiobuttonGroup> group);
87 
88 		void show(anim::type animated);
89 		void destroy(FnMut<void()> done);
90 
91 		[[nodiscard]] bool hasShadow() const;
92 		void createShadow();
93 		void destroyShadow();
94 
95 		[[nodiscard]] bool isEmpty() const;
96 		[[nodiscard]] bool isGood() const;
97 		[[nodiscard]] bool isTooLong() const;
98 		[[nodiscard]] bool isCorrect() const;
99 		[[nodiscard]] bool hasFocus() const;
100 		void setFocus() const;
101 		void clearValue();
102 
103 		void setPlaceholder() const;
104 		void removePlaceholder() const;
105 
106 		not_null<Ui::InputField*> field() const;
107 
108 		[[nodiscard]] PollAnswer toPollAnswer(int index) const;
109 
110 		[[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const;
111 
112 	private:
113 		void createRemove();
114 		void createWarning();
115 		void toggleCorrectSpace(bool visible);
116 		void updateFieldGeometry();
117 
118 		base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap;
119 		not_null<Ui::RpWidget*> _content;
120 		base::unique_qptr<Ui::FadeWrapScaled<Ui::Radiobutton>> _correct;
121 		Ui::Animations::Simple _correctShown;
122 		bool _hasCorrect = false;
123 		Ui::InputField *_field = nullptr;
124 		base::unique_qptr<Ui::PlainShadow> _shadow;
125 		base::unique_qptr<Ui::CrossButton> _remove;
126 		rpl::variable<bool> *_removeAlways = nullptr;
127 
128 	};
129 
130 	[[nodiscard]] bool full() const;
131 	[[nodiscard]] bool correctShadows() const;
132 	void fixShadows();
133 	void removeEmptyTail();
134 	void addEmptyOption();
135 	void checkLastOption();
136 	void validateState();
137 	void fixAfterErase();
138 	void destroy(std::unique_ptr<Option> option);
139 	void removeDestroyed(not_null<Option*> field);
140 	int findField(not_null<Ui::InputField*> field) const;
141 	[[nodiscard]] auto createChooseCorrectGroup()
142 		-> std::shared_ptr<Ui::RadiobuttonGroup>;
143 
144 	not_null<QWidget*> _outer;
145 	not_null<Ui::VerticalLayout*> _container;
146 	const not_null<Main::Session*> _session;
147 	std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;
148 	int _position = 0;
149 	std::vector<std::unique_ptr<Option>> _list;
150 	std::vector<std::unique_ptr<Option>> _destroyed;
151 	rpl::variable<int> _usedCount = 0;
152 	bool _hasOptions = false;
153 	bool _isValid = false;
154 	bool _hasCorrect = false;
155 	rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
156 	rpl::event_stream<> _backspaceInFront;
157 	rpl::event_stream<> _tabbed;
158 
159 };
160 
InitField(not_null<QWidget * > container,not_null<Ui::InputField * > field,not_null<Main::Session * > session)161 void InitField(
162 		not_null<QWidget*> container,
163 		not_null<Ui::InputField*> field,
164 		not_null<Main::Session*> session) {
165 	field->setInstantReplaces(Ui::InstantReplaces::Default());
166 	field->setInstantReplacesEnabled(
167 		Core::App().settings().replaceEmojiValue());
168 	auto options = Ui::Emoji::SuggestionsController::Options();
169 	options.suggestExactFirstWord = false;
170 	Ui::Emoji::SuggestionsController::Init(
171 		container,
172 		field,
173 		session,
174 		options);
175 }
176 
CreateWarningLabel(not_null<QWidget * > parent,not_null<Ui::InputField * > field,int valueLimit,int warnLimit)177 not_null<Ui::FlatLabel*> CreateWarningLabel(
178 		not_null<QWidget*> parent,
179 		not_null<Ui::InputField*> field,
180 		int valueLimit,
181 		int warnLimit) {
182 	const auto result = Ui::CreateChild<Ui::FlatLabel>(
183 		parent.get(),
184 		QString(),
185 		st::createPollWarning);
186 	result->setAttribute(Qt::WA_TransparentForMouseEvents);
187 	QObject::connect(field, &Ui::InputField::changed, [=] {
188 		Ui::PostponeCall(crl::guard(field, [=] {
189 			const auto length = field->getLastText().size();
190 			const auto value = valueLimit - length;
191 			const auto shown = (value < warnLimit)
192 				&& (field->height() > st::createPollOptionField.heightMin);
193 			result->setRichText((value >= 0)
194 				? QString::number(value)
195 				: textcmdLink(1, QString::number(value)));
196 			result->setVisible(shown);
197 		}));
198 	});
199 	return result;
200 }
201 
FocusAtEnd(not_null<Ui::InputField * > field)202 void FocusAtEnd(not_null<Ui::InputField*> field) {
203 	field->setFocus();
204 	field->setCursorPosition(field->getLastText().size());
205 	field->ensureCursorVisible();
206 }
207 
Option(not_null<QWidget * > outer,not_null<Ui::VerticalLayout * > container,not_null<Main::Session * > session,int position,std::shared_ptr<Ui::RadiobuttonGroup> group)208 Options::Option::Option(
209 	not_null<QWidget*> outer,
210 	not_null<Ui::VerticalLayout*> container,
211 	not_null<Main::Session*> session,
212 	int position,
213 	std::shared_ptr<Ui::RadiobuttonGroup> group)
214 : _wrap(container->insert(
215 	position,
216 	object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
217 		container,
218 		object_ptr<Ui::RpWidget>(container))))
219 , _content(_wrap->entity())
220 , _field(
221 	Ui::CreateChild<Ui::InputField>(
222 		_content.get(),
223 		st::createPollOptionField,
224 		Ui::InputField::Mode::NoNewlines,
225 		tr::lng_polls_create_option_add())) {
226 	InitField(outer, _field, session);
227 	_field->setMaxLength(kOptionLimit + kErrorLimit);
228 	_field->show();
229 	_field->customTab(true);
230 
231 	_wrap->hide(anim::type::instant);
232 
233 	_content->widthValue(
234 	) | rpl::start_with_next([=] {
235 		updateFieldGeometry();
236 	}, _field->lifetime());
237 
238 	_field->heightValue(
239 	) | rpl::start_with_next([=](int height) {
240 		_content->resize(_content->width(), height);
241 	}, _field->lifetime());
242 
243 	QObject::connect(_field, &Ui::InputField::changed, [=] {
244 		Ui::PostponeCall(crl::guard(_field, [=] {
245 			if (_hasCorrect) {
246 				_correct->toggle(isGood(), anim::type::normal);
247 			}
248 		}));
249 	});
250 
251 	createShadow();
252 	createRemove();
253 	createWarning();
254 	enableChooseCorrect(group);
255 	_correctShown.stop();
256 	if (_correct) {
257 		_correct->finishAnimating();
258 	}
259 	updateFieldGeometry();
260 }
261 
hasShadow() const262 bool Options::Option::hasShadow() const {
263 	return (_shadow != nullptr);
264 }
265 
createShadow()266 void Options::Option::createShadow() {
267 	Expects(_content != nullptr);
268 
269 	if (_shadow) {
270 		return;
271 	}
272 	_shadow.reset(Ui::CreateChild<Ui::PlainShadow>(field().get()));
273 	_shadow->show();
274 	field()->sizeValue(
275 	) | rpl::start_with_next([=](QSize size) {
276 		const auto left = st::createPollFieldPadding.left();
277 		_shadow->setGeometry(
278 			left,
279 			size.height() - st::lineWidth,
280 			size.width() - left,
281 			st::lineWidth);
282 	}, _shadow->lifetime());
283 }
284 
destroyShadow()285 void Options::Option::destroyShadow() {
286 	_shadow = nullptr;
287 }
288 
createRemove()289 void Options::Option::createRemove() {
290 	using namespace rpl::mappers;
291 
292 	const auto field = this->field();
293 	auto &lifetime = field->lifetime();
294 
295 	const auto remove = Ui::CreateChild<Ui::CrossButton>(
296 		field.get(),
297 		st::createPollOptionRemove);
298 	remove->hide(anim::type::instant);
299 
300 	const auto toggle = lifetime.make_state<rpl::variable<bool>>(false);
301 	_removeAlways = lifetime.make_state<rpl::variable<bool>>(false);
302 
303 	QObject::connect(field, &Ui::InputField::changed, [=] {
304 		// Don't capture 'this'! Because Option is a value type.
305 		*toggle = !field->getLastText().isEmpty();
306 	});
307 	rpl::combine(
308 		toggle->value(),
309 		_removeAlways->value(),
310 		_1 || _2
311 	) | rpl::start_with_next([=](bool shown) {
312 		remove->toggle(shown, anim::type::normal);
313 	}, remove->lifetime());
314 
315 	field->widthValue(
316 	) | rpl::start_with_next([=](int width) {
317 		remove->moveToRight(
318 			st::createPollOptionRemovePosition.x(),
319 			st::createPollOptionRemovePosition.y(),
320 			width);
321 	}, remove->lifetime());
322 
323 	_remove.reset(remove);
324 }
325 
createWarning()326 void Options::Option::createWarning() {
327 	using namespace rpl::mappers;
328 
329 	const auto field = this->field();
330 	const auto warning = CreateWarningLabel(
331 		field,
332 		field,
333 		kOptionLimit,
334 		kWarnOptionLimit);
335 	rpl::combine(
336 		field->sizeValue(),
337 		warning->sizeValue()
338 	) | rpl::start_with_next([=](QSize size, QSize label) {
339 		warning->moveToLeft(
340 			(size.width()
341 				- label.width()
342 				- st::createPollWarningPosition.x()),
343 			(size.height()
344 				- label.height()
345 				- st::createPollWarningPosition.y()),
346 			size.width());
347 	}, warning->lifetime());
348 }
349 
isEmpty() const350 bool Options::Option::isEmpty() const {
351 	return field()->getLastText().trimmed().isEmpty();
352 }
353 
isGood() const354 bool Options::Option::isGood() const {
355 	return !field()->getLastText().trimmed().isEmpty() && !isTooLong();
356 }
357 
isTooLong() const358 bool Options::Option::isTooLong() const {
359 	return (field()->getLastText().size() > kOptionLimit);
360 }
361 
isCorrect() const362 bool Options::Option::isCorrect() const {
363 	return isGood() && _correct && _correct->entity()->Checkbox::checked();
364 }
365 
hasFocus() const366 bool Options::Option::hasFocus() const {
367 	return field()->hasFocus();
368 }
369 
setFocus() const370 void Options::Option::setFocus() const {
371 	FocusAtEnd(field());
372 }
373 
clearValue()374 void Options::Option::clearValue() {
375 	field()->setText(QString());
376 }
377 
setPlaceholder() const378 void Options::Option::setPlaceholder() const {
379 	field()->setPlaceholder(tr::lng_polls_create_option_add());
380 }
381 
toggleRemoveAlways(bool toggled)382 void Options::Option::toggleRemoveAlways(bool toggled) {
383 	*_removeAlways = toggled;
384 }
385 
enableChooseCorrect(std::shared_ptr<Ui::RadiobuttonGroup> group)386 void Options::Option::enableChooseCorrect(
387 		std::shared_ptr<Ui::RadiobuttonGroup> group) {
388 	if (!group) {
389 		if (_correct) {
390 			_hasCorrect = false;
391 			_correct->hide(anim::type::normal);
392 			toggleCorrectSpace(false);
393 		}
394 		return;
395 	}
396 	static auto Index = 0;
397 	const auto button = Ui::CreateChild<Ui::FadeWrapScaled<Ui::Radiobutton>>(
398 		_content.get(),
399 		object_ptr<Ui::Radiobutton>(
400 			_content.get(),
401 			group,
402 			++Index,
403 			QString(),
404 			st::defaultCheckbox));
405 	button->entity()->resize(
406 		button->entity()->height(),
407 		button->entity()->height());
408 	button->hide(anim::type::instant);
409 	_content->sizeValue(
410 	) | rpl::start_with_next([=](QSize size) {
411 		const auto left = st::createPollFieldPadding.left();
412 		button->moveToLeft(
413 			left,
414 			(size.height() - button->heightNoMargins()) / 2);
415 	}, button->lifetime());
416 	_correct.reset(button);
417 	_hasCorrect = true;
418 	if (isGood()) {
419 		_correct->show(anim::type::normal);
420 	} else {
421 		_correct->hide(anim::type::instant);
422 	}
423 	toggleCorrectSpace(true);
424 }
425 
toggleCorrectSpace(bool visible)426 void Options::Option::toggleCorrectSpace(bool visible) {
427 	_correctShown.start(
428 		[=] { updateFieldGeometry(); },
429 		visible ? 0. : 1.,
430 		visible ? 1. : 0.,
431 		st::fadeWrapDuration);
432 }
433 
updateFieldGeometry()434 void Options::Option::updateFieldGeometry() {
435 	const auto shown = _correctShown.value(_hasCorrect ? 1. : 0.);
436 	const auto skip = st::defaultRadio.diameter
437 		+ st::defaultCheckbox.textPosition.x();
438 	const auto left = anim::interpolate(0, skip, shown);
439 	_field->resizeToWidth(_content->width() - left);
440 	_field->moveToLeft(left, 0);
441 }
442 
field() const443 not_null<Ui::InputField*> Options::Option::field() const {
444 	return _field;
445 }
446 
removePlaceholder() const447 void Options::Option::removePlaceholder() const {
448 	field()->setPlaceholder(rpl::single(QString()));
449 }
450 
toPollAnswer(int index) const451 PollAnswer Options::Option::toPollAnswer(int index) const {
452 	Expects(index >= 0 && index < kMaxOptionsCount);
453 
454 	auto result = PollAnswer{
455 		field()->getLastText().trimmed(),
456 		QByteArray(1, ('0' + index))
457 	};
458 	result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
459 	return result;
460 }
461 
removeClicks() const462 rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
463 	return _remove->clicks();
464 }
465 
Options(not_null<QWidget * > outer,not_null<Ui::VerticalLayout * > container,not_null<Main::Session * > session,bool chooseCorrectEnabled)466 Options::Options(
467 	not_null<QWidget*> outer,
468 	not_null<Ui::VerticalLayout*> container,
469 	not_null<Main::Session*> session,
470 	bool chooseCorrectEnabled)
471 : _outer(outer)
472 , _container(container)
473 , _session(session)
474 , _chooseCorrectGroup(chooseCorrectEnabled
475 	? createChooseCorrectGroup()
476 	: nullptr)
477 , _position(_container->count()) {
478 	checkLastOption();
479 }
480 
full() const481 bool Options::full() const {
482 	return (_list.size() == kMaxOptionsCount);
483 }
484 
hasOptions() const485 bool Options::hasOptions() const {
486 	return _hasOptions;
487 }
488 
isValid() const489 bool Options::isValid() const {
490 	return _isValid;
491 }
492 
hasCorrect() const493 bool Options::hasCorrect() const {
494 	return _hasCorrect;
495 }
496 
usedCount() const497 rpl::producer<int> Options::usedCount() const {
498 	return _usedCount.value();
499 }
500 
scrollToWidget() const501 rpl::producer<not_null<QWidget*>> Options::scrollToWidget() const {
502 	return _scrollToWidget.events();
503 }
504 
backspaceInFront() const505 rpl::producer<> Options::backspaceInFront() const {
506 	return _backspaceInFront.events();
507 }
508 
tabbed() const509 rpl::producer<> Options::tabbed() const {
510 	return _tabbed.events();
511 }
512 
show(anim::type animated)513 void Options::Option::show(anim::type animated) {
514 	_wrap->show(animated);
515 }
516 
destroy(FnMut<void ()> done)517 void Options::Option::destroy(FnMut<void()> done) {
518 	if (anim::Disabled() || _wrap->isHidden()) {
519 		Ui::PostponeCall(std::move(done));
520 		return;
521 	}
522 	_wrap->hide(anim::type::normal);
523 	base::call_delayed(
524 		st::slideWrapDuration * 2,
525 		_content.get(),
526 		std::move(done));
527 }
528 
toPollAnswers() const529 std::vector<PollAnswer> Options::toPollAnswers() const {
530 	auto result = std::vector<PollAnswer>();
531 	result.reserve(_list.size());
532 	auto counter = int(0);
533 	const auto makeAnswer = [&](const std::unique_ptr<Option> &option) {
534 		return option->toPollAnswer(counter++);
535 	};
536 	ranges::copy(
537 		_list
538 		| ranges::views::filter(&Option::isGood)
539 		| ranges::views::transform(makeAnswer),
540 		ranges::back_inserter(result));
541 	return result;
542 }
543 
focusFirst()544 void Options::focusFirst() {
545 	Expects(!_list.empty());
546 
547 	_list.front()->setFocus();
548 }
549 
createChooseCorrectGroup()550 std::shared_ptr<Ui::RadiobuttonGroup> Options::createChooseCorrectGroup() {
551 	auto result = std::make_shared<Ui::RadiobuttonGroup>(0);
552 	result->setChangedCallback([=](int) {
553 		validateState();
554 	});
555 	return result;
556 }
557 
enableChooseCorrect(bool enabled)558 void Options::enableChooseCorrect(bool enabled) {
559 	_chooseCorrectGroup = enabled
560 		? createChooseCorrectGroup()
561 		: nullptr;
562 	for (auto &option : _list) {
563 		option->enableChooseCorrect(_chooseCorrectGroup);
564 	}
565 	validateState();
566 }
567 
correctShadows() const568 bool Options::correctShadows() const {
569 	// Last one should be without shadow.
570 	const auto noShadow = ranges::find(
571 		_list,
572 		true,
573 		ranges::not_fn(&Option::hasShadow));
574 	return (noShadow == end(_list) - 1);
575 }
576 
fixShadows()577 void Options::fixShadows() {
578 	if (correctShadows()) {
579 		return;
580 	}
581 	for (auto &option : _list) {
582 		option->createShadow();
583 	}
584 	_list.back()->destroyShadow();
585 }
586 
removeEmptyTail()587 void Options::removeEmptyTail() {
588 	// Only one option at the end of options list can be empty.
589 	// Remove all other trailing empty options.
590 	// Only last empty and previous option have non-empty placeholders.
591 	const auto focused = ranges::find_if(
592 		_list,
593 		&Option::hasFocus);
594 	const auto end = _list.end();
595 	const auto reversed = ranges::views::reverse(_list);
596 	const auto emptyItem = ranges::find_if(
597 		reversed,
598 		ranges::not_fn(&Option::isEmpty)).base();
599 	const auto focusLast = (focused > emptyItem) && (focused < end);
600 	if (emptyItem == end) {
601 		return;
602 	}
603 	if (focusLast) {
604 		(*emptyItem)->setFocus();
605 	}
606 	for (auto i = emptyItem + 1; i != end; ++i) {
607 		destroy(std::move(*i));
608 	}
609 	_list.erase(emptyItem + 1, end);
610 	fixAfterErase();
611 }
612 
destroy(std::unique_ptr<Option> option)613 void Options::destroy(std::unique_ptr<Option> option) {
614 	const auto value = option.get();
615 	option->destroy([=] { removeDestroyed(value); });
616 	_destroyed.push_back(std::move(option));
617 }
618 
fixAfterErase()619 void Options::fixAfterErase() {
620 	Expects(!_list.empty());
621 
622 	const auto last = _list.end() - 1;
623 	(*last)->setPlaceholder();
624 	(*last)->toggleRemoveAlways(false);
625 	if (last != begin(_list)) {
626 		(*(last - 1))->setPlaceholder();
627 		(*(last - 1))->toggleRemoveAlways(false);
628 	}
629 	fixShadows();
630 }
631 
addEmptyOption()632 void Options::addEmptyOption() {
633 	if (full()) {
634 		return;
635 	} else if (!_list.empty() && _list.back()->isEmpty()) {
636 		return;
637 	}
638 	if (_list.size() > 1) {
639 		(*(_list.end() - 2))->removePlaceholder();
640 		(*(_list.end() - 2))->toggleRemoveAlways(true);
641 	}
642 	_list.push_back(std::make_unique<Option>(
643 		_outer,
644 		_container,
645 		_session,
646 		_position + _list.size() + _destroyed.size(),
647 		_chooseCorrectGroup));
648 	const auto field = _list.back()->field();
649 	QObject::connect(field, &Ui::InputField::submitted, [=] {
650 		const auto index = findField(field);
651 		if (_list[index]->isGood() && index + 1 < _list.size()) {
652 			_list[index + 1]->setFocus();
653 		}
654 	});
655 	QObject::connect(field, &Ui::InputField::changed, [=] {
656 		Ui::PostponeCall(crl::guard(field, [=] {
657 			validateState();
658 		}));
659 	});
660 	QObject::connect(field, &Ui::InputField::focused, [=] {
661 		_scrollToWidget.fire_copy(field);
662 	});
663 	QObject::connect(field, &Ui::InputField::tabbed, [=] {
664 		const auto index = findField(field);
665 		if (index + 1 < _list.size()) {
666 			_list[index + 1]->setFocus();
667 		} else {
668 			_tabbed.fire({});
669 		}
670 	});
671 	base::install_event_filter(field, [=](not_null<QEvent*> event) {
672 		if (event->type() != QEvent::KeyPress
673 			|| !field->getLastText().isEmpty()) {
674 			return base::EventFilterResult::Continue;
675 		}
676 		const auto key = static_cast<QKeyEvent*>(event.get())->key();
677 		if (key != Qt::Key_Backspace) {
678 			return base::EventFilterResult::Continue;
679 		}
680 
681 		const auto index = findField(field);
682 		if (index > 0) {
683 			_list[index - 1]->setFocus();
684 		} else {
685 			_backspaceInFront.fire({});
686 		}
687 		return base::EventFilterResult::Cancel;
688 	});
689 
690 	_list.back()->removeClicks(
691 	) | rpl::take(1) | rpl::start_with_next([=] {
692 		Ui::PostponeCall(crl::guard(field, [=] {
693 			Expects(!_list.empty());
694 
695 			const auto item = begin(_list) + findField(field);
696 			if (item == _list.end() - 1) {
697 				(*item)->clearValue();
698 				return;
699 			}
700 			if ((*item)->hasFocus()) {
701 				(*(item + 1))->setFocus();
702 			}
703 			destroy(std::move(*item));
704 			_list.erase(item);
705 			fixAfterErase();
706 			validateState();
707 		}));
708 	}, field->lifetime());
709 
710 	_list.back()->show((_list.size() == 1)
711 		? anim::type::instant
712 		: anim::type::normal);
713 	fixShadows();
714 }
715 
removeDestroyed(not_null<Option * > option)716 void Options::removeDestroyed(not_null<Option*> option) {
717 	const auto i = ranges::find(
718 		_destroyed,
719 		option.get(),
720 		&std::unique_ptr<Option>::get);
721 	Assert(i != end(_destroyed));
722 	_destroyed.erase(i);
723 }
724 
validateState()725 void Options::validateState() {
726 	checkLastOption();
727 	_hasOptions = (ranges::count_if(_list, &Option::isGood) > 1);
728 	_isValid = _hasOptions && ranges::none_of(_list, &Option::isTooLong);
729 	_hasCorrect = ranges::any_of(_list, &Option::isCorrect);
730 
731 	const auto lastEmpty = !_list.empty() && _list.back()->isEmpty();
732 	_usedCount = _list.size() - (lastEmpty ? 1 : 0);
733 }
734 
findField(not_null<Ui::InputField * > field) const735 int Options::findField(not_null<Ui::InputField*> field) const {
736 	const auto result = ranges::find(
737 		_list,
738 		field,
739 		&Option::field) - begin(_list);
740 
741 	Ensures(result >= 0 && result < _list.size());
742 	return result;
743 }
744 
checkLastOption()745 void Options::checkLastOption() {
746 	removeEmptyTail();
747 	addEmptyOption();
748 }
749 
750 } // namespace
751 
CreatePollBox(QWidget *,not_null<Window::SessionController * > controller,PollData::Flags chosen,PollData::Flags disabled,Api::SendType sendType,SendMenu::Type sendMenuType)752 CreatePollBox::CreatePollBox(
753 	QWidget*,
754 	not_null<Window::SessionController*> controller,
755 	PollData::Flags chosen,
756 	PollData::Flags disabled,
757 	Api::SendType sendType,
758 	SendMenu::Type sendMenuType)
759 : _controller(controller)
760 , _chosen(chosen)
761 , _disabled(disabled)
762 , _sendType(sendType)
763 , _sendMenuType(sendMenuType) {
764 }
765 
submitRequests() const766 rpl::producer<CreatePollBox::Result> CreatePollBox::submitRequests() const {
767 	return _submitRequests.events();
768 }
769 
setInnerFocus()770 void CreatePollBox::setInnerFocus() {
771 	_setInnerFocus();
772 }
773 
submitFailed(const QString & error)774 void CreatePollBox::submitFailed(const QString &error) {
775 	Ui::Toast::Show(error);
776 }
777 
setupQuestion(not_null<Ui::VerticalLayout * > container)778 not_null<Ui::InputField*> CreatePollBox::setupQuestion(
779 		not_null<Ui::VerticalLayout*> container) {
780 	using namespace Settings;
781 
782 	const auto session = &_controller->session();
783 	AddSubsectionTitle(container, tr::lng_polls_create_question());
784 	const auto question = container->add(
785 		object_ptr<Ui::InputField>(
786 			container,
787 			st::createPollField,
788 			Ui::InputField::Mode::MultiLine,
789 			tr::lng_polls_create_question_placeholder()),
790 		st::createPollFieldPadding);
791 	InitField(getDelegate()->outerContainer(), question, session);
792 	question->setMaxLength(kQuestionLimit + kErrorLimit);
793 	question->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
794 	question->customTab(true);
795 
796 	const auto warning = CreateWarningLabel(
797 		container,
798 		question,
799 		kQuestionLimit,
800 		kWarnQuestionLimit);
801 	rpl::combine(
802 		question->geometryValue(),
803 		warning->sizeValue()
804 	) | rpl::start_with_next([=](QRect geometry, QSize label) {
805 		warning->moveToLeft(
806 			(container->width()
807 				- label.width()
808 				- st::createPollWarningPosition.x()),
809 			(geometry.y()
810 				- st::createPollFieldPadding.top()
811 				- st::settingsSubsectionTitlePadding.bottom()
812 				- st::settingsSubsectionTitle.style.font->height
813 				+ st::settingsSubsectionTitle.style.font->ascent
814 				- st::createPollWarning.style.font->ascent),
815 			geometry.width());
816 	}, warning->lifetime());
817 
818 	return question;
819 }
820 
setupSolution(not_null<Ui::VerticalLayout * > container,rpl::producer<bool> shown)821 not_null<Ui::InputField*> CreatePollBox::setupSolution(
822 		not_null<Ui::VerticalLayout*> container,
823 		rpl::producer<bool> shown) {
824 	using namespace Settings;
825 
826 	const auto outer = container->add(
827 		object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
828 			container,
829 			object_ptr<Ui::VerticalLayout>(container))
830 	)->setDuration(0)->toggleOn(std::move(shown));
831 	const auto inner = outer->entity();
832 
833 	const auto session = &_controller->session();
834 	AddSkip(inner);
835 	AddSubsectionTitle(inner, tr::lng_polls_solution_title());
836 	const auto solution = inner->add(
837 		object_ptr<Ui::InputField>(
838 			inner,
839 			st::createPollSolutionField,
840 			Ui::InputField::Mode::MultiLine,
841 			tr::lng_polls_solution_placeholder()),
842 		st::createPollFieldPadding);
843 	InitField(getDelegate()->outerContainer(), solution, session);
844 	solution->setMaxLength(kSolutionLimit + kErrorLimit);
845 	solution->setInstantReplaces(Ui::InstantReplaces::Default());
846 	solution->setInstantReplacesEnabled(
847 		Core::App().settings().replaceEmojiValue());
848 	solution->setMarkdownReplacesEnabled(rpl::single(true));
849 	solution->setEditLinkCallback(
850 		DefaultEditLinkCallback(_controller, solution));
851 	solution->customTab(true);
852 
853 	const auto warning = CreateWarningLabel(
854 		inner,
855 		solution,
856 		kSolutionLimit,
857 		kWarnSolutionLimit);
858 	rpl::combine(
859 		solution->geometryValue(),
860 		warning->sizeValue()
861 	) | rpl::start_with_next([=](QRect geometry, QSize label) {
862 		warning->moveToLeft(
863 			(inner->width()
864 				- label.width()
865 				- st::createPollWarningPosition.x()),
866 			(geometry.y()
867 				- st::createPollFieldPadding.top()
868 				- st::settingsSubsectionTitlePadding.bottom()
869 				- st::settingsSubsectionTitle.style.font->height
870 				+ st::settingsSubsectionTitle.style.font->ascent
871 				- st::createPollWarning.style.font->ascent),
872 			geometry.width());
873 	}, warning->lifetime());
874 
875 	inner->add(
876 		object_ptr<Ui::FlatLabel>(
877 			inner,
878 			tr::lng_polls_solution_about(),
879 			st::boxDividerLabel),
880 		st::createPollFieldTitlePadding);
881 
882 	return solution;
883 }
884 
setupContent()885 object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
886 	using namespace Settings;
887 
888 	const auto id = base::RandomValue<uint64>();
889 	const auto error = lifetime().make_state<Errors>(Error::Question);
890 
891 	auto result = object_ptr<Ui::VerticalLayout>(this);
892 	const auto container = result.data();
893 
894 	const auto question = setupQuestion(container);
895 	AddDivider(container);
896 	AddSkip(container);
897 	container->add(
898 		object_ptr<Ui::FlatLabel>(
899 			container,
900 			tr::lng_polls_create_options(),
901 			st::settingsSubsectionTitle),
902 		st::createPollFieldTitlePadding);
903 	const auto options = lifetime().make_state<Options>(
904 		getDelegate()->outerContainer(),
905 		container,
906 		&_controller->session(),
907 		(_chosen & PollData::Flag::Quiz));
908 	auto limit = options->usedCount() | rpl::after_next([=](int count) {
909 		setCloseByEscape(!count);
910 		setCloseByOutsideClick(!count);
911 	}) | rpl::map([=](int count) {
912 		return (count < kMaxOptionsCount)
913 			? tr::lng_polls_create_limit(tr::now, lt_count, kMaxOptionsCount - count)
914 			: tr::lng_polls_create_maximum(tr::now);
915 	}) | rpl::after_next([=] {
916 		container->resizeToWidth(container->widthNoMargins());
917 	});
918 	container->add(
919 		object_ptr<Ui::DividerLabel>(
920 			container,
921 			object_ptr<Ui::FlatLabel>(
922 				container,
923 				std::move(limit),
924 				st::boxDividerLabel),
925 			st::createPollLimitPadding));
926 
927 	connect(question, &Ui::InputField::tabbed, [=] {
928 		options->focusFirst();
929 	});
930 
931 	AddSkip(container);
932 	AddSubsectionTitle(container, tr::lng_polls_create_settings());
933 
934 	const auto anonymous = (!(_disabled & PollData::Flag::PublicVotes))
935 		? container->add(
936 			object_ptr<Ui::Checkbox>(
937 				container,
938 				tr::lng_polls_create_anonymous(tr::now),
939 				!(_chosen & PollData::Flag::PublicVotes),
940 				st::defaultCheckbox),
941 			st::createPollCheckboxMargin)
942 		: nullptr;
943 	const auto hasMultiple = !(_chosen & PollData::Flag::Quiz)
944 		|| !(_disabled & PollData::Flag::Quiz);
945 	const auto multiple = hasMultiple
946 		? container->add(
947 			object_ptr<Ui::Checkbox>(
948 				container,
949 				tr::lng_polls_create_multiple_choice(tr::now),
950 				(_chosen & PollData::Flag::MultiChoice),
951 				st::defaultCheckbox),
952 			st::createPollCheckboxMargin)
953 		: nullptr;
954 	const auto quiz = container->add(
955 		object_ptr<Ui::Checkbox>(
956 			container,
957 			tr::lng_polls_create_quiz_mode(tr::now),
958 			(_chosen & PollData::Flag::Quiz),
959 			st::defaultCheckbox),
960 		st::createPollCheckboxMargin);
961 
962 	const auto solution = setupSolution(
963 		container,
964 		rpl::single(quiz->checked()) | rpl::then(quiz->checkedChanges()));
965 
966 	options->tabbed(
967 	) | rpl::start_with_next([=] {
968 		if (quiz->checked()) {
969 			solution->setFocus();
970 		} else {
971 			question->setFocus();
972 		}
973 	}, question->lifetime());
974 
975 	connect(solution, &Ui::InputField::tabbed, [=] {
976 		question->setFocus();
977 	});
978 
979 	quiz->setDisabled(_disabled & PollData::Flag::Quiz);
980 	if (multiple) {
981 		multiple->setDisabled((_disabled & PollData::Flag::MultiChoice)
982 			|| (_chosen & PollData::Flag::Quiz));
983 		multiple->events(
984 		) | rpl::filter([=](not_null<QEvent*> e) {
985 			return (e->type() == QEvent::MouseButtonPress) && quiz->checked();
986 		}) | rpl::start_with_next([=] {
987 			Ui::Toast::Show(tr::lng_polls_create_one_answer(tr::now));
988 		}, multiple->lifetime());
989 	}
990 
991 	using namespace rpl::mappers;
992 	quiz->checkedChanges(
993 	) | rpl::start_with_next([=](bool checked) {
994 		if (multiple) {
995 			if (checked && multiple->checked()) {
996 				multiple->setChecked(false);
997 			}
998 			multiple->setDisabled(checked
999 				|| (_disabled & PollData::Flag::MultiChoice));
1000 		}
1001 		options->enableChooseCorrect(checked);
1002 	}, quiz->lifetime());
1003 
1004 	const auto isValidQuestion = [=] {
1005 		const auto text = question->getLastText().trimmed();
1006 		return !text.isEmpty() && (text.size() <= kQuestionLimit);
1007 	};
1008 
1009 	connect(question, &Ui::InputField::submitted, [=] {
1010 		if (isValidQuestion()) {
1011 			options->focusFirst();
1012 		}
1013 	});
1014 
1015 	_setInnerFocus = [=] {
1016 		question->setFocusFast();
1017 	};
1018 
1019 	const auto collectResult = [=] {
1020 		using Flag = PollData::Flag;
1021 		auto result = PollData(&_controller->session().data(), id);
1022 		result.question = question->getLastText().trimmed();
1023 		result.answers = options->toPollAnswers();
1024 		const auto solutionWithTags = quiz->checked()
1025 			? solution->getTextWithAppliedMarkdown()
1026 			: TextWithTags();
1027 		result.solution = TextWithEntities{
1028 			solutionWithTags.text,
1029 			TextUtilities::ConvertTextTagsToEntities(solutionWithTags.tags)
1030 		};
1031 		const auto publicVotes = (anonymous && !anonymous->checked());
1032 		const auto multiChoice = (multiple && multiple->checked());
1033 		result.setFlags(Flag(0)
1034 			| (publicVotes ? Flag::PublicVotes : Flag(0))
1035 			| (multiChoice ? Flag::MultiChoice : Flag(0))
1036 			| (quiz->checked() ? Flag::Quiz : Flag(0)));
1037 		return result;
1038 	};
1039 	const auto collectError = [=] {
1040 		if (isValidQuestion()) {
1041 			*error &= ~Error::Question;
1042 		} else {
1043 			*error |= Error::Question;
1044 		}
1045 		if (!options->hasOptions()) {
1046 			*error |= Error::Options;
1047 		} else if (!options->isValid()) {
1048 			*error |= Error::Other;
1049 		} else {
1050 			*error &= ~(Error::Options | Error::Other);
1051 		}
1052 		if (quiz->checked() && !options->hasCorrect()) {
1053 			*error |= Error::Correct;
1054 		} else {
1055 			*error &= ~Error::Correct;
1056 		}
1057 		if (quiz->checked()
1058 			&& solution->getLastText().trimmed().size() > kSolutionLimit) {
1059 			*error |= Error::Solution;
1060 		} else {
1061 			*error &= ~Error::Solution;
1062 		}
1063 	};
1064 	const auto showError = [](tr::phrase<> text) {
1065 		Ui::Toast::Show(text(tr::now));
1066 	};
1067 	const auto send = [=](Api::SendOptions sendOptions) {
1068 		collectError();
1069 		if (*error & Error::Question) {
1070 			showError(tr::lng_polls_choose_question);
1071 			question->setFocus();
1072 		} else if (*error & Error::Options) {
1073 			showError(tr::lng_polls_choose_answers);
1074 			options->focusFirst();
1075 		} else if (*error & Error::Correct) {
1076 			showError(tr::lng_polls_choose_correct);
1077 		} else if (*error & Error::Solution) {
1078 			solution->showError();
1079 		} else if (!*error) {
1080 			_submitRequests.fire({ collectResult(), sendOptions });
1081 		}
1082 	};
1083 	const auto sendSilent = [=] {
1084 		send({ .silent = true });
1085 	};
1086 	const auto sendScheduled = [=] {
1087 		_controller->show(
1088 			HistoryView::PrepareScheduleBox(
1089 				this,
1090 				SendMenu::Type::Scheduled,
1091 				send),
1092 			Ui::LayerOption::KeepOther);
1093 	};
1094 
1095 	options->scrollToWidget(
1096 	) | rpl::start_with_next([=](not_null<QWidget*> widget) {
1097 		scrollToWidget(widget);
1098 	}, lifetime());
1099 
1100 	options->backspaceInFront(
1101 	) | rpl::start_with_next([=] {
1102 		FocusAtEnd(question);
1103 	}, lifetime());
1104 
1105 	const auto isNormal = (_sendType == Api::SendType::Normal);
1106 
1107 	const auto submit = addButton(
1108 		isNormal
1109 			? tr::lng_polls_create_button()
1110 			: tr::lng_schedule_button(),
1111 		[=] { isNormal ? send({}) : sendScheduled(); });
1112 	const auto sendMenuType = [=] {
1113 		collectError();
1114 		return (*error)
1115 			? SendMenu::Type::Disabled
1116 			: _sendMenuType;
1117 	};
1118 	SendMenu::SetupMenuAndShortcuts(
1119 		submit.data(),
1120 		sendMenuType,
1121 		sendSilent,
1122 		sendScheduled);
1123 	addButton(tr::lng_cancel(), [=] { closeBox(); });
1124 
1125 	return result;
1126 }
1127 
prepare()1128 void CreatePollBox::prepare() {
1129 	setTitle(tr::lng_polls_create_title());
1130 
1131 	const auto inner = setInnerWidget(setupContent());
1132 
1133 	setDimensionsToContent(st::boxWideWidth, inner);
1134 }
1135