/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/create_poll_box.h" #include "lang/lang_keys.h" #include "data/data_poll.h" #include "ui/toast/toast.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/fade_wrap.h" #include "ui/widgets/input_fields.h" #include "ui/widgets/shadow.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/toast/toast.h" #include "main/main_session.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/message_field.h" #include "history/view/history_view_schedule_box.h" #include "settings/settings_common.h" #include "base/unique_qptr.h" #include "base/event_filter.h" #include "base/call_delayed.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_settings.h" namespace { constexpr auto kQuestionLimit = 255; constexpr auto kMaxOptionsCount = PollData::kMaxOptions; constexpr auto kOptionLimit = 100; constexpr auto kWarnQuestionLimit = 80; constexpr auto kWarnOptionLimit = 30; constexpr auto kErrorLimit = 99; class Options { public: Options( not_null<QWidget*> outer, not_null<Ui::VerticalLayout*> container, not_null<Main::Session*> session, bool chooseCorrectEnabled); [[nodiscard]] bool hasOptions() const; [[nodiscard]] bool isValid() const; [[nodiscard]] bool hasCorrect() const; [[nodiscard]] std::vector<PollAnswer> toPollAnswers() const; void focusFirst(); void enableChooseCorrect(bool enabled); [[nodiscard]] rpl::producer<int> usedCount() const; [[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const; [[nodiscard]] rpl::producer<> backspaceInFront() const; private: class Option { public: Option( not_null<QWidget*> outer, not_null<Ui::VerticalLayout*> container, not_null<Main::Session*> session, int position, std::shared_ptr<Ui::RadiobuttonGroup> group); Option(const Option &other) = delete; Option &operator=(const Option &other) = delete; void toggleRemoveAlways(bool toggled); void enableChooseCorrect( std::shared_ptr<Ui::RadiobuttonGroup> group); void show(anim::type animated); void destroy(FnMut<void()> done); [[nodisacrd]] bool hasShadow() const; void createShadow(); void destroyShadow(); [[nodiscard]] bool isEmpty() const; [[nodiscard]] bool isGood() const; [[nodiscard]] bool isTooLong() const; [[nodiscard]] bool isCorrect() const; [[nodiscard]] bool hasFocus() const; void setFocus() const; void clearValue(); void setPlaceholder() const; void removePlaceholder() const; not_null<Ui::InputField*> field() const; [[nodiscard]] PollAnswer toPollAnswer(int index) const; [[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const; private: void createRemove(); void createWarning(); void toggleCorrectSpace(bool visible); void updateFieldGeometry(); base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap; not_null<Ui::RpWidget*> _content; base::unique_qptr<Ui::FadeWrapScaled<Ui::Radiobutton>> _correct; Ui::Animations::Simple _correctShown; bool _hasCorrect = false; Ui::InputField *_field = nullptr; base::unique_qptr<Ui::PlainShadow> _shadow; base::unique_qptr<Ui::CrossButton> _remove; rpl::variable<bool> *_removeAlways = nullptr; }; [[nodiscard]] bool full() const; [[nodiscard]] bool correctShadows() const; void fixShadows(); void removeEmptyTail(); void addEmptyOption(); void checkLastOption(); void validateState(); void fixAfterErase(); void destroy(std::unique_ptr<Option> option); void removeDestroyed(not_null<Option*> field); int findField(not_null<Ui::InputField*> field) const; [[nodiscard]] auto createChooseCorrectGroup() -> std::shared_ptr<Ui::RadiobuttonGroup>; not_null<QWidget*> _outer; not_null<Ui::VerticalLayout*> _container; const not_null<Main::Session*> _session; std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup; int _position = 0; std::vector<std::unique_ptr<Option>> _list; std::vector<std::unique_ptr<Option>> _destroyed; rpl::variable<int> _usedCount = 0; bool _hasOptions = false; bool _isValid = false; bool _hasCorrect = false; rpl::event_stream<not_null<QWidget*>> _scrollToWidget; rpl::event_stream<> _backspaceInFront; }; void InitField( not_null<QWidget*> container, not_null<Ui::InputField*> field, not_null<Main::Session*> session) { field->setInstantReplaces(Ui::InstantReplaces::Default()); field->setInstantReplacesEnabled( session->settings().replaceEmojiValue()); auto options = Ui::Emoji::SuggestionsController::Options(); options.suggestExactFirstWord = false; Ui::Emoji::SuggestionsController::Init( container, field, session, options); } not_null<Ui::FlatLabel*> CreateWarningLabel( not_null<QWidget*> parent, not_null<Ui::InputField*> field, int valueLimit, int warnLimit) { const auto result = Ui::CreateChild<Ui::FlatLabel>( parent.get(), QString(), st::createPollWarning); result->setAttribute(Qt::WA_TransparentForMouseEvents); QObject::connect(field, &Ui::InputField::changed, [=] { Ui::PostponeCall(crl::guard(field, [=] { const auto length = field->getLastText().size(); const auto value = valueLimit - length; const auto shown = (value < warnLimit) && (field->height() > st::createPollOptionField.heightMin); result->setRichText((value >= 0) ? QString::number(value) : textcmdLink(1, QString::number(value))); result->setVisible(shown); })); }); return result; } void FocusAtEnd(not_null<Ui::InputField*> field) { field->setFocus(); field->setCursorPosition(field->getLastText().size()); field->ensureCursorVisible(); } Options::Option::Option( not_null<QWidget*> outer, not_null<Ui::VerticalLayout*> container, not_null<Main::Session*> session, int position, std::shared_ptr<Ui::RadiobuttonGroup> group) : _wrap(container->insert( position, object_ptr<Ui::SlideWrap<Ui::RpWidget>>( container, object_ptr<Ui::RpWidget>(container)))) , _content(_wrap->entity()) , _field( Ui::CreateChild<Ui::InputField>( _content.get(), st::createPollOptionField, Ui::InputField::Mode::NoNewlines, tr::lng_polls_create_option_add())) { InitField(outer, _field, session); _field->setMaxLength(kOptionLimit + kErrorLimit); _field->show(); _wrap->hide(anim::type::instant); _content->widthValue( ) | rpl::start_with_next([=] { updateFieldGeometry(); }, _field->lifetime()); _field->heightValue( ) | rpl::start_with_next([=](int height) { _content->resize(_content->width(), height); }, _field->lifetime()); QObject::connect(_field, &Ui::InputField::changed, [=] { Ui::PostponeCall(crl::guard(_field, [=] { if (_hasCorrect) { _correct->toggle(isGood(), anim::type::normal); } })); }); createShadow(); createRemove(); createWarning(); enableChooseCorrect(group); _correctShown.stop(); if (_correct) { _correct->finishAnimating(); } updateFieldGeometry(); } bool Options::Option::hasShadow() const { return (_shadow != nullptr); } void Options::Option::createShadow() { Expects(_content != nullptr); if (_shadow) { return; } _shadow.reset(Ui::CreateChild<Ui::PlainShadow>(field().get())); _shadow->show(); field()->sizeValue( ) | rpl::start_with_next([=](QSize size) { const auto left = st::createPollFieldPadding.left(); _shadow->setGeometry( left, size.height() - st::lineWidth, size.width() - left, st::lineWidth); }, _shadow->lifetime()); } void Options::Option::destroyShadow() { _shadow = nullptr; } void Options::Option::createRemove() { using namespace rpl::mappers; const auto field = this->field(); auto &lifetime = field->lifetime(); const auto remove = Ui::CreateChild<Ui::CrossButton>( field.get(), st::createPollOptionRemove); remove->hide(anim::type::instant); const auto toggle = lifetime.make_state<rpl::variable<bool>>(false); _removeAlways = lifetime.make_state<rpl::variable<bool>>(false); QObject::connect(field, &Ui::InputField::changed, [=] { // Don't capture 'this'! Because Option is a value type. *toggle = !field->getLastText().isEmpty(); }); rpl::combine( toggle->value(), _removeAlways->value(), _1 || _2 ) | rpl::start_with_next([=](bool shown) { remove->toggle(shown, anim::type::normal); }, remove->lifetime()); field->widthValue( ) | rpl::start_with_next([=](int width) { remove->moveToRight( st::createPollOptionRemovePosition.x(), st::createPollOptionRemovePosition.y(), width); }, remove->lifetime()); _remove.reset(remove); } void Options::Option::createWarning() { using namespace rpl::mappers; const auto field = this->field(); const auto warning = CreateWarningLabel( field, field, kOptionLimit, kWarnOptionLimit); rpl::combine( field->sizeValue(), warning->sizeValue() ) | rpl::start_with_next([=](QSize size, QSize label) { warning->moveToLeft( (size.width() - label.width() - st::createPollWarningPosition.x()), (size.height() - label.height() - st::createPollWarningPosition.y()), size.width()); }, warning->lifetime()); } bool Options::Option::isEmpty() const { return field()->getLastText().trimmed().isEmpty(); } bool Options::Option::isGood() const { return !field()->getLastText().trimmed().isEmpty() && !isTooLong(); } bool Options::Option::isTooLong() const { return (field()->getLastText().size() > kOptionLimit); } bool Options::Option::isCorrect() const { return isGood() && _correct && _correct->entity()->Checkbox::checked(); } bool Options::Option::hasFocus() const { return field()->hasFocus(); } void Options::Option::setFocus() const { FocusAtEnd(field()); } void Options::Option::clearValue() { field()->setText(QString()); } void Options::Option::setPlaceholder() const { field()->setPlaceholder(tr::lng_polls_create_option_add()); } void Options::Option::toggleRemoveAlways(bool toggled) { *_removeAlways = toggled; } void Options::Option::enableChooseCorrect( std::shared_ptr<Ui::RadiobuttonGroup> group) { if (!group) { if (_correct) { _hasCorrect = false; _correct->hide(anim::type::normal); toggleCorrectSpace(false); } return; } static auto Index = 0; const auto button = Ui::CreateChild<Ui::FadeWrapScaled<Ui::Radiobutton>>( _content.get(), object_ptr<Ui::Radiobutton>( _content.get(), group, ++Index, QString(), st::defaultCheckbox)); button->entity()->resize( button->entity()->height(), button->entity()->height()); button->hide(anim::type::instant); _content->sizeValue( ) | rpl::start_with_next([=](QSize size) { const auto left = st::createPollFieldPadding.left(); button->moveToLeft( left, (size.height() - button->heightNoMargins()) / 2); }, button->lifetime()); _correct.reset(button); _hasCorrect = true; if (isGood()) { _correct->show(anim::type::normal); } else { _correct->hide(anim::type::instant); } toggleCorrectSpace(true); } void Options::Option::toggleCorrectSpace(bool visible) { _correctShown.start( [=] { updateFieldGeometry(); }, visible ? 0. : 1., visible ? 1. : 0., st::fadeWrapDuration); } void Options::Option::updateFieldGeometry() { const auto shown = _correctShown.value(_hasCorrect ? 1. : 0.); const auto skip = st::defaultRadio.diameter + st::defaultCheckbox.textPosition.x(); const auto left = anim::interpolate(0, skip, shown); const auto width = _content->width() - left; _field->resizeToWidth(_content->width() - left); _field->moveToLeft(left, 0); } not_null<Ui::InputField*> Options::Option::field() const { return _field; } void Options::Option::removePlaceholder() const { field()->setPlaceholder(rpl::single(QString())); } PollAnswer Options::Option::toPollAnswer(int index) const { Expects(index >= 0 && index < kMaxOptionsCount); auto result = PollAnswer{ field()->getLastText().trimmed(), QByteArray(1, ('0' + index)) }; result.correct = _correct ? _correct->entity()->Checkbox::checked() : false; return result; } rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const { return _remove->clicks(); } Options::Options( not_null<QWidget*> outer, not_null<Ui::VerticalLayout*> container, not_null<Main::Session*> session, bool chooseCorrectEnabled) : _outer(outer) , _container(container) , _session(session) , _chooseCorrectGroup(chooseCorrectEnabled ? createChooseCorrectGroup() : nullptr) , _position(_container->count()) { checkLastOption(); } bool Options::full() const { return (_list.size() == kMaxOptionsCount); } bool Options::hasOptions() const { return _hasOptions; } bool Options::isValid() const { return _isValid; } bool Options::hasCorrect() const { return _hasCorrect; } rpl::producer<int> Options::usedCount() const { return _usedCount.value(); } rpl::producer<not_null<QWidget*>> Options::scrollToWidget() const { return _scrollToWidget.events(); } rpl::producer<> Options::backspaceInFront() const { return _backspaceInFront.events(); } void Options::Option::show(anim::type animated) { _wrap->show(animated); } void Options::Option::destroy(FnMut<void()> done) { if (anim::Disabled() || _wrap->isHidden()) { Ui::PostponeCall(std::move(done)); return; } _wrap->hide(anim::type::normal); base::call_delayed( st::slideWrapDuration * 2, _content.get(), std::move(done)); } std::vector<PollAnswer> Options::toPollAnswers() const { auto result = std::vector<PollAnswer>(); result.reserve(_list.size()); auto counter = int(0); const auto makeAnswer = [&](const std::unique_ptr<Option> &option) { return option->toPollAnswer(counter++); }; ranges::copy( _list | ranges::view::filter(&Option::isGood) | ranges::view::transform(makeAnswer), ranges::back_inserter(result)); return result; } void Options::focusFirst() { Expects(!_list.empty()); _list.front()->setFocus(); } std::shared_ptr<Ui::RadiobuttonGroup> Options::createChooseCorrectGroup() { auto result = std::make_shared<Ui::RadiobuttonGroup>(0); result->setChangedCallback([=](int) { validateState(); }); return result; } void Options::enableChooseCorrect(bool enabled) { _chooseCorrectGroup = enabled ? createChooseCorrectGroup() : nullptr; for (auto &option : _list) { option->enableChooseCorrect(_chooseCorrectGroup); } validateState(); } bool Options::correctShadows() const { // Last one should be without shadow. const auto noShadow = ranges::find( _list, true, ranges::not_fn(&Option::hasShadow)); return (noShadow == end(_list) - 1); } void Options::fixShadows() { if (correctShadows()) { return; } for (auto &option : _list) { option->createShadow(); } _list.back()->destroyShadow(); } void Options::removeEmptyTail() { // Only one option at the end of options list can be empty. // Remove all other trailing empty options. // Only last empty and previous option have non-empty placeholders. const auto focused = ranges::find_if( _list, &Option::hasFocus); const auto end = _list.end(); const auto reversed = ranges::view::reverse(_list); const auto emptyItem = ranges::find_if( reversed, ranges::not_fn(&Option::isEmpty)).base(); const auto focusLast = (focused > emptyItem) && (focused < end); if (emptyItem == end) { return; } if (focusLast) { (*emptyItem)->setFocus(); } for (auto i = emptyItem + 1; i != end; ++i) { destroy(std::move(*i)); } _list.erase(emptyItem + 1, end); fixAfterErase(); } void Options::destroy(std::unique_ptr<Option> option) { const auto value = option.get(); option->destroy([=] { removeDestroyed(value); }); _destroyed.push_back(std::move(option)); } void Options::fixAfterErase() { Expects(!_list.empty()); const auto last = _list.end() - 1; (*last)->setPlaceholder(); (*last)->toggleRemoveAlways(false); if (last != begin(_list)) { (*(last - 1))->setPlaceholder(); (*(last - 1))->toggleRemoveAlways(false); } fixShadows(); } void Options::addEmptyOption() { if (full()) { return; } else if (!_list.empty() && _list.back()->isEmpty()) { return; } if (_list.size() > 1) { (*(_list.end() - 2))->removePlaceholder(); (*(_list.end() - 2))->toggleRemoveAlways(true); } _list.push_back(std::make_unique<Option>( _outer, _container, _session, _position + _list.size() + _destroyed.size(), _chooseCorrectGroup)); const auto field = _list.back()->field(); QObject::connect(field, &Ui::InputField::submitted, [=] { const auto index = findField(field); if (_list[index]->isGood() && index + 1 < _list.size()) { _list[index + 1]->setFocus(); } }); QObject::connect(field, &Ui::InputField::changed, [=] { Ui::PostponeCall(crl::guard(field, [=] { validateState(); })); }); QObject::connect(field, &Ui::InputField::focused, [=] { _scrollToWidget.fire_copy(field); }); base::install_event_filter(field, [=](not_null<QEvent*> event) { if (event->type() != QEvent::KeyPress || !field->getLastText().isEmpty()) { return base::EventFilterResult::Continue; } const auto key = static_cast<QKeyEvent*>(event.get())->key(); if (key != Qt::Key_Backspace) { return base::EventFilterResult::Continue; } const auto index = findField(field); if (index > 0) { _list[index - 1]->setFocus(); } else { _backspaceInFront.fire({}); } return base::EventFilterResult::Cancel; }); _list.back()->removeClicks( ) | rpl::take(1) | rpl::start_with_next([=] { Ui::PostponeCall(crl::guard(field, [=] { Expects(!_list.empty()); const auto item = begin(_list) + findField(field); if (item == _list.end() - 1) { (*item)->clearValue(); return; } if ((*item)->hasFocus()) { (*(item + 1))->setFocus(); } destroy(std::move(*item)); _list.erase(item); fixAfterErase(); validateState(); })); }, field->lifetime()); _list.back()->show((_list.size() == 1) ? anim::type::instant : anim::type::normal); fixShadows(); } void Options::removeDestroyed(not_null<Option*> option) { const auto i = ranges::find( _destroyed, option.get(), &std::unique_ptr<Option>::get); Assert(i != end(_destroyed)); _destroyed.erase(i); } void Options::validateState() { checkLastOption(); _hasOptions = (ranges::count_if(_list, &Option::isGood) > 1); _isValid = _hasOptions && (ranges::find_if(_list, &Option::isTooLong) == end(_list)); _hasCorrect = ranges::find_if(_list, &Option::isCorrect) != end(_list); const auto lastEmpty = !_list.empty() && _list.back()->isEmpty(); _usedCount = _list.size() - (lastEmpty ? 1 : 0); } int Options::findField(not_null<Ui::InputField*> field) const { const auto result = ranges::find( _list, field, &Option::field) - begin(_list); Ensures(result >= 0 && result < _list.size()); return result; } void Options::checkLastOption() { removeEmptyTail(); addEmptyOption(); } } // namespace CreatePollBox::CreatePollBox( QWidget*, not_null<Main::Session*> session, PollData::Flags chosen, PollData::Flags disabled, Api::SendType sendType) : _session(session) , _chosen(chosen) , _disabled(disabled) , _sendType(sendType) { } rpl::producer<CreatePollBox::Result> CreatePollBox::submitRequests() const { return _submitRequests.events(); } void CreatePollBox::setInnerFocus() { _setInnerFocus(); } void CreatePollBox::submitFailed(const QString &error) { Ui::Toast::Show(error); } not_null<Ui::InputField*> CreatePollBox::setupQuestion( not_null<Ui::VerticalLayout*> container) { using namespace Settings; AddSubsectionTitle(container, tr::lng_polls_create_question()); const auto question = container->add( object_ptr<Ui::InputField>( container, st::createPollField, Ui::InputField::Mode::MultiLine, tr::lng_polls_create_question_placeholder()), st::createPollFieldPadding); InitField(getDelegate()->outerContainer(), question, _session); question->setMaxLength(kQuestionLimit + kErrorLimit); const auto warning = CreateWarningLabel( container, question, kQuestionLimit, kWarnQuestionLimit); rpl::combine( question->geometryValue(), warning->sizeValue() ) | rpl::start_with_next([=](QRect geometry, QSize label) { warning->moveToLeft( (container->width() - label.width() - st::createPollWarningPosition.x()), (geometry.y() - st::createPollFieldPadding.top() - st::settingsSubsectionTitlePadding.bottom() - st::settingsSubsectionTitle.style.font->height + st::settingsSubsectionTitle.style.font->ascent - st::createPollWarning.style.font->ascent), geometry.width()); }, warning->lifetime()); return question; } object_ptr<Ui::RpWidget> CreatePollBox::setupContent() { using namespace Settings; const auto id = rand_value<uint64>(); const auto error = lifetime().make_state<Errors>(Error::Question); auto result = object_ptr<Ui::VerticalLayout>(this); const auto container = result.data(); const auto question = setupQuestion(container); AddDivider(container); AddSkip(container); container->add( object_ptr<Ui::FlatLabel>( container, tr::lng_polls_create_options(), st::settingsSubsectionTitle), st::createPollFieldTitlePadding); const auto options = lifetime().make_state<Options>( getDelegate()->outerContainer(), container, _session, (_chosen & PollData::Flag::Quiz)); auto limit = options->usedCount() | rpl::after_next([=](int count) { setCloseByEscape(!count); setCloseByOutsideClick(!count); }) | rpl::map([=](int count) { return (count < kMaxOptionsCount) ? tr::lng_polls_create_limit(tr::now, lt_count, kMaxOptionsCount - count) : tr::lng_polls_create_maximum(tr::now); }) | rpl::after_next([=] { container->resizeToWidth(container->widthNoMargins()); }); container->add( object_ptr<Ui::DividerLabel>( container, object_ptr<Ui::FlatLabel>( container, std::move(limit), st::boxDividerLabel), st::createPollLimitPadding)); AddSkip(container); AddSubsectionTitle(container, tr::lng_polls_create_settings()); const auto anonymous = (!(_disabled & PollData::Flag::PublicVotes)) ? container->add( object_ptr<Ui::Checkbox>( container, tr::lng_polls_create_anonymous(tr::now), !(_chosen & PollData::Flag::PublicVotes), st::defaultCheckbox), st::createPollCheckboxMargin) : nullptr; const auto hasMultiple = !(_chosen & PollData::Flag::Quiz) || !(_disabled & PollData::Flag::Quiz); const auto multiple = hasMultiple ? container->add( object_ptr<Ui::Checkbox>( container, tr::lng_polls_create_multiple_choice(tr::now), (_chosen & PollData::Flag::MultiChoice), st::defaultCheckbox), st::createPollCheckboxMargin) : nullptr; const auto quiz = container->add( object_ptr<Ui::Checkbox>( container, tr::lng_polls_create_quiz_mode(tr::now), (_chosen & PollData::Flag::Quiz), st::defaultCheckbox), st::createPollCheckboxMargin); quiz->setDisabled(_disabled & PollData::Flag::Quiz); if (multiple) { multiple->setDisabled((_disabled & PollData::Flag::MultiChoice) || (_chosen & PollData::Flag::Quiz)); multiple->events( ) | rpl::filter([=](not_null<QEvent*> e) { return (e->type() == QEvent::MouseButtonPress) && quiz->checked(); }) | rpl::start_with_next([=] { Ui::Toast::Show(tr::lng_polls_create_one_answer(tr::now)); }, multiple->lifetime()); } using namespace rpl::mappers; quiz->checkedChanges( ) | rpl::start_with_next([=](bool checked) { if (multiple) { if (checked && multiple->checked()) { multiple->setChecked(false); } multiple->setDisabled(checked || (_disabled & PollData::Flag::MultiChoice)); } options->enableChooseCorrect(checked); }, quiz->lifetime()); const auto isValidQuestion = [=] { const auto text = question->getLastText().trimmed(); return !text.isEmpty() && (text.size() <= kQuestionLimit); }; connect(question, &Ui::InputField::submitted, [=] { if (isValidQuestion()) { options->focusFirst(); } }); _setInnerFocus = [=] { question->setFocusFast(); }; const auto collectResult = [=] { using Flag = PollData::Flag; auto result = PollData(&_session->data(), id); result.question = question->getLastText().trimmed(); result.answers = options->toPollAnswers(); const auto publicVotes = (anonymous && !anonymous->checked()); const auto multiChoice = (multiple && multiple->checked()); result.setFlags(Flag(0) | (publicVotes ? Flag::PublicVotes : Flag(0)) | (multiChoice ? Flag::MultiChoice : Flag(0)) | (quiz->checked() ? Flag::Quiz : Flag(0))); return result; }; const auto collectError = [=] { if (isValidQuestion()) { *error &= ~Error::Question; } else { *error |= Error::Question; } if (!options->hasOptions()) { *error |= Error::Options; } else if (!options->isValid()) { *error |= Error::Other; } else { *error &= ~(Error::Options | Error::Other); } if (quiz->checked() && !options->hasCorrect()) { *error |= Error::Correct; } else { *error &= ~Error::Correct; } }; const auto showError = [=](const QString &text) { Ui::Toast::Show(text); }; const auto send = [=](Api::SendOptions sendOptions) { collectError(); if (*error & Error::Question) { showError(tr::lng_polls_choose_question(tr::now)); question->setFocus(); } else if (*error & Error::Options) { showError(tr::lng_polls_choose_answers(tr::now)); options->focusFirst(); } else if (*error & Error::Correct) { showError(tr::lng_polls_choose_correct(tr::now)); } else if (!*error) { _submitRequests.fire({ collectResult(), sendOptions }); } }; const auto sendSilent = [=] { auto options = Api::SendOptions(); options.silent = true; send(options); }; const auto sendScheduled = [=] { Ui::show( HistoryView::PrepareScheduleBox( this, SendMenuType::Scheduled, send), Ui::LayerOption::KeepOther); }; options->scrollToWidget( ) | rpl::start_with_next([=](not_null<QWidget*> widget) { scrollToWidget(widget); }, lifetime()); options->backspaceInFront( ) | rpl::start_with_next([=] { FocusAtEnd(question); }, lifetime()); const auto submit = addButton( tr::lng_polls_create_button(), [=] { send({}); }); if (_sendType == Api::SendType::Normal) { const auto sendMenuType = [=] { collectError(); return *error ? SendMenuType::Disabled : SendMenuType::Scheduled; }; SetupSendMenuAndShortcuts( submit.data(), sendMenuType, sendSilent, sendScheduled); } addButton(tr::lng_cancel(), [=] { closeBox(); }); return result; } void CreatePollBox::prepare() { setTitle(tr::lng_polls_create_title()); const auto inner = setInnerWidget(setupContent()); setDimensionsToContent(st::boxWideWidth, inner); }