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