diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 7f68a80adc..a3e8307f25 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6870,6 +6870,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_create_shuffle_options_about" = "Answers appear in random order for each voter."; "lng_polls_create_set_correct_answer" = "Set Correct Answer"; "lng_polls_create_set_correct_answer_about" = "Mark one option as the right answer."; +"lng_polls_create_limit_duration" = "Limit Duration"; +"lng_polls_create_limit_duration_about" = "Automatically close the poll at a set time."; +"lng_polls_create_poll_duration" = "Poll Duration"; +"lng_polls_create_poll_ends" = "Poll ends"; +"lng_polls_create_hide_results" = "Hide results"; +"lng_polls_create_hide_results_about" = "If you switch this on, results will appear only after the poll closes."; +"lng_polls_create_duration_custom" = "Custom"; +"lng_polls_create_deadline_title" = "Deadline"; +"lng_polls_create_deadline_button" = "Set Deadline"; +"lng_polls_create_deadline_expired" = "The poll deadline has already passed. Please choose a new time."; "lng_polls_create_anonymous" = "Anonymous Voting"; "lng_polls_create_multiple_choice" = "Multiple Answers"; "lng_polls_create_quiz_mode" = "Quiz Mode"; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index 980b2b93ff..f30accef5f 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/create_poll_box.h" #include "base/call_delayed.h" +#include "base/unixtime.h" #include "boxes/premium_limits_box.h" #include "base/event_filter.h" #include "base/random.h" @@ -47,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/dynamic_thumbnails.h" #include "ui/effects/radial_animation.h" #include "ui/effects/ripple_animation.h" +#include "ui/effects/ttl_icon.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/text/text_utilities.h" @@ -58,6 +60,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" +#include "ui/boxes/choose_date_time.h" +#include "ui/text/format_values.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/shadow.h" #include "ui/wrap/fade_wrap.h" @@ -68,6 +72,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "apiwrap.h" #include "styles/style_boxes.h" +#include "styles/style_dialogs.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" // defaultComposeFiles. #include "styles/style_layers.h" @@ -1329,6 +1334,51 @@ void Options::checkLastOption() { addEmptyOption(); } +class DurationIconAction final : public Ui::Menu::Action { +public: + DurationIconAction( + not_null parent, + const style::Menu &st, + not_null action, + const QString &tinyText); + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + QString _tinyText; + +}; + +DurationIconAction::DurationIconAction( + not_null parent, + const style::Menu &st, + not_null action, + const QString &tinyText) +: Ui::Menu::Action(parent, st, action, nullptr, nullptr) +, _tinyText(tinyText) { +} + +void DurationIconAction::paintEvent(QPaintEvent *e) { + Ui::Menu::Action::paintEvent(e); + + const auto &st = this->st(); + const auto iconPos = st.itemIconPosition; + const auto size = st::createPollDurationIconSize; + const auto &pos = st::createPollDurationIconPosition; + const auto rect = QRect( + iconPos.x() + pos.x(), + iconPos.y() + pos.y(), + size, + size); + const auto innerRect = rect - st::createPollDurationIconMargins; + + Painter p(this); + PainterHighQualityEnabler hq(p); + + Ui::PaintTimerIcon(p, innerRect, _tinyText, st::menuIconColor->c); +} + } // namespace CreatePollBox::CreatePollBox( @@ -1554,6 +1604,8 @@ object_ptr CreatePollBox::setupContent() { rpl::event_stream addOptionsForceOff; rpl::event_stream revotingForceOff; rpl::event_stream quizForceOff; + rpl::variable closePeriod = 0; + rpl::variable closeDate = TimeId(0); std::shared_ptr descriptionMedia = std::make_shared(); std::shared_ptr solutionMedia @@ -1561,6 +1613,7 @@ object_ptr CreatePollBox::setupContent() { std::weak_ptr stickerTarget; base::flat_map uploads; base::unique_qptr mediaMenu; + base::unique_qptr durationMenu; base::unique_qptr stickerPanel; std::unique_ptr prepareQueue; }; @@ -2541,6 +2594,128 @@ object_ptr CreatePollBox::setupContent() { | rpl::then(state->quizForceOff.events()), st::detailedSettingsButtonStyle); + const auto duration = AddPollToggleButton( + container, + tr::lng_polls_create_limit_duration(), + tr::lng_polls_create_limit_duration_about(), + { + .icon = &st::pollBoxFilledPollDeadlineIcon, + .background = &st::settingsIconBg1, + }, + rpl::single(false), + st::detailedSettingsButtonStyle); + + const auto durationWrap = container->add( + object_ptr>( + container, + object_ptr(container)) + )->toggleOn( + rpl::single(duration->toggled()) + | rpl::then(duration->toggledChanges())); + const auto durationInner = durationWrap->entity(); + + auto pollEndsLabelText = state->closePeriod.value( + ) | rpl::map([=](int) { + const auto date = state->closeDate.current(); + if (date > 0) { + return langDateTime(base::unixtime::parse(date)); + } + const auto period = state->closePeriod.current(); + if (period > 0) { + const auto target = base::unixtime::now() + period; + return langDateTime(base::unixtime::parse(target)); + } + return QString(); + }); + state->closeDate.value( + ) | rpl::on_next([=](TimeId) { + state->closePeriod.force_assign(state->closePeriod.current()); + }, durationInner->lifetime()); + + const auto pollEndsLabel = AddButtonWithLabel( + durationInner, + tr::lng_polls_create_poll_ends(), + std::move(pollEndsLabelText), + st::settingsButtonNoIcon); + + const auto show = uiShow(); + pollEndsLabel->setClickedCallback([=] { + state->durationMenu = base::make_unique_q( + pollEndsLabel, + st::popupMenuWithIcons); + const auto &menuSt = state->durationMenu->st().menu; + const auto presets = { + 3600, + 3 * 3600, + 8 * 3600, + 24 * 3600, + 72 * 3600, + }; + for (const auto seconds : presets) { + const auto text = Ui::FormatMuteFor(seconds); + auto item = base::make_unique_q( + state->durationMenu->menu(), + menuSt, + Ui::Menu::CreateAction( + state->durationMenu->menu().get(), + text, + [=] { + state->closePeriod = seconds; + state->closeDate = TimeId(0); + }), + Ui::FormatTTLTiny(seconds)); + state->durationMenu->addAction(std::move(item)); + } + state->durationMenu->addAction( + tr::lng_polls_create_duration_custom(tr::now), + [=] { + const auto now = base::unixtime::now(); + const auto current = (state->closeDate.current() > now) + ? state->closeDate.current() + : (state->closePeriod.current() > 0) + ? (now + state->closePeriod.current()) + : (now + 24 * 3600); + show->show(Box([=](not_null box) { + Ui::ChooseDateTimeBox(box, { + .title = tr::lng_polls_create_deadline_title(), + .submit = tr::lng_polls_create_deadline_button(), + .done = [=](TimeId time) { + state->closeDate = time; + state->closePeriod = 0; + box->closeBox(); + }, + .min = [=] { return base::unixtime::now() + 60; }, + .time = current, + .max = [=] { + return base::unixtime::now() + 365 * 24 * 3600; + }, + }); + })); + }, + &st::menuIconCustomize); + state->durationMenu->popup(QCursor::pos()); + }); + + duration->toggledChanges( + ) | rpl::on_next([=](bool checked) { + if (checked && state->closePeriod.current() == 0 + && state->closeDate.current() == 0) { + state->closePeriod = 24 * 3600; + } + }, duration->lifetime()); + + const auto hideResults = durationInner->add( + object_ptr( + durationInner, + tr::lng_polls_create_hide_results(), + st::settingsButtonNoIcon) + )->toggleOn(rpl::single(false)); + + Ui::AddSkip(durationInner); + Ui::AddDividerText( + durationInner, + tr::lng_polls_create_hide_results_about()); + const auto solution = setupSolution( container, rpl::single(quiz->toggled()) | rpl::then(quiz->toggledChanges())); @@ -2632,15 +2807,31 @@ object_ptr CreatePollBox::setupContent() { if (quiz->toggled()) { result.solutionMedia = state->solutionMedia->media; } + if (duration->toggled()) { + const auto closeDate = state->closeDate.current(); + const auto closePeriod = state->closePeriod.current(); + if (closeDate > 0) { + result.closeDate = closeDate; + result.closePeriod = closeDate - base::unixtime::now(); + } else if (closePeriod > 0) { + result.closePeriod = closePeriod; + result.closeDate = base::unixtime::now() + closePeriod; + } + } const auto publicVotes = (showWhoVoted && showWhoVoted->toggled()); const auto multiChoice = multiple->toggled(); + const auto hideResultsEnabled = duration->toggled() + && hideResults->toggled(); result.setFlags(Flag(0) | (publicVotes ? Flag::PublicVotes : Flag(0)) | (multiChoice ? Flag::MultiChoice : Flag(0)) | ((addOptions && addOptions->toggled()) ? Flag::OpenAnswers : Flag(0)) | (!revoting->toggled() ? Flag::RevotingDisabled : Flag(0)) | (shuffle->toggled() ? Flag::ShuffleAnswers : Flag(0)) - | (quiz->toggled() ? Flag::Quiz : Flag(0))); + | (quiz->toggled() ? Flag::Quiz : Flag(0)) + | (hideResultsEnabled + ? Flag::HideResultsUntilClose + : Flag(0))); auto text = TextWithEntities{ descriptionWithTags.text, TextUtilities::ConvertTextTagsToEntities( @@ -2684,6 +2875,23 @@ object_ptr CreatePollBox::setupContent() { } else { state->error &= ~Error::Media; } + if (duration->toggled()) { + const auto now = base::unixtime::now(); + const auto closeDate = state->closeDate.current(); + const auto closePeriod = state->closePeriod.current(); + const auto deadline = (closeDate > 0) + ? closeDate + : (closePeriod > 0) + ? (now + closePeriod) + : 0; + if (deadline > 0 && deadline <= now) { + state->error |= Error::Deadline; + } else { + state->error &= ~Error::Deadline; + } + } else { + state->error &= ~Error::Deadline; + } }; const auto showError = [show = uiShow()]( tr::phrase<> text) { @@ -2703,6 +2911,8 @@ object_ptr CreatePollBox::setupContent() { solution->showError(); } else if (state->error & Error::Media) { showError(tr::lng_polls_media_uploading); + } else if (state->error & Error::Deadline) { + showError(tr::lng_polls_create_deadline_expired); } else if (!state->error) { auto result = collectResult(); result.options = sendOptions; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.h b/Telegram/SourceFiles/boxes/create_poll_box.h index 22cd771671..b96e709794 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.h +++ b/Telegram/SourceFiles/boxes/create_poll_box.h @@ -66,6 +66,7 @@ private: Other = 0x08, Solution = 0x10, Media = 0x20, + Deadline = 0x40, }; friend constexpr inline bool is_flag_type(Error) { return true; } using Errors = base::flags; diff --git a/Telegram/SourceFiles/boxes/polls.style b/Telegram/SourceFiles/boxes/polls.style index ad660d6b0a..31842114c7 100644 --- a/Telegram/SourceFiles/boxes/polls.style +++ b/Telegram/SourceFiles/boxes/polls.style @@ -212,6 +212,10 @@ pollAttach: IconButton(defaultIconButton) { ripple: defaultRippleAnimationBgOver; } +createPollDurationIconSize: 20px; +createPollDurationIconPosition: point(1px, 5px); +createPollDurationIconMargins: margins(2px, 2px, 2px, 2px); + pollDescriptionFieldPadding: margins(22px, 5px, 11px, 5px); pollDescriptionField: InputField(createPollField) { textMargins: margins(0px, 4px, pollAttachTextSkip, 4px);