3314 linhas
92 KiB
C++
3314 linhas
92 KiB
C++
/*
|
|
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 "poll/poll_media_upload.h"
|
|
#include "base/call_delayed.h"
|
|
#include "base/qt/qt_key_modifiers.h"
|
|
#include "base/unixtime.h"
|
|
#include "boxes/premium_limits_box.h"
|
|
#include "base/event_filter.h"
|
|
#include "base/random.h"
|
|
#include "base/unique_qptr.h"
|
|
#include "countries/countries_instance.h"
|
|
#include "chat_helpers/emoji_suggestions_widget.h"
|
|
#include "chat_helpers/message_field.h"
|
|
#include "chat_helpers/tabbed_panel.h"
|
|
#include "chat_helpers/tabbed_selector.h"
|
|
#include "core/file_utilities.h"
|
|
#include "core/mime_type.h"
|
|
#include "core/application.h"
|
|
#include "core/core_settings.h"
|
|
#include "core/shortcuts.h"
|
|
#include "core/ui_integration.h"
|
|
#include "ui/power_saving.h"
|
|
#include "data/data_cloud_file.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_file_origin.h"
|
|
#include "data/data_location.h"
|
|
#include "data/data_poll.h"
|
|
#include "data/data_peer.h"
|
|
#include "data/data_photo.h"
|
|
#include "data/data_session.h"
|
|
#include "data/data_user.h"
|
|
#include "data/stickers/data_custom_emoji.h"
|
|
#include "history/view/media/menu/history_view_poll_menu.h"
|
|
#include "history/view/history_view_schedule_box.h"
|
|
#include "info/channel_statistics/boosts/giveaway/select_countries_box.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "layout/layout_document_generic_preview.h"
|
|
#include "main/main_app_config.h"
|
|
#include "mainwidget.h"
|
|
#include "mainwindow.h"
|
|
#include "platform/platform_file_utilities.h"
|
|
#include "main/main_session.h"
|
|
#include "menu/menu_send.h"
|
|
#include "settings/detailed_settings_button.h"
|
|
#include "settings/settings_common.h"
|
|
#include "storage/file_upload.h"
|
|
#include "storage/localimageloader.h"
|
|
#include "storage/storage_account.h"
|
|
#include "storage/storage_media_prepare.h"
|
|
#include "ui/controls/emoji_button.h"
|
|
#include "ui/controls/emoji_button_factory.h"
|
|
#include "ui/controls/location_picker.h"
|
|
#include "ui/chat/attach/attach_prepare.h"
|
|
#include "ui/dynamic_image.h"
|
|
#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"
|
|
#include "ui/toast/toast.h"
|
|
#include "ui/vertical_list.h"
|
|
#include "ui/widgets/buttons.h"
|
|
#include "ui/widgets/dropdown_menu.h"
|
|
#include "ui/widgets/menu/menu_action.h"
|
|
#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/scroll_area.h"
|
|
#include "ui/widgets/shadow.h"
|
|
#include "ui/wrap/fade_wrap.h"
|
|
#include "ui/wrap/slide_wrap.h"
|
|
#include "ui/wrap/vertical_layout.h"
|
|
#include "ui/wrap/vertical_layout_reorder.h"
|
|
#include "ui/ui_utility.h"
|
|
#include "window/section_widget.h"
|
|
#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"
|
|
#include "styles/style_menu_icons.h"
|
|
#include "styles/style_overview.h"
|
|
#include "styles/style_polls.h"
|
|
#include "styles/style_settings.h"
|
|
|
|
#include <QtCore/QBuffer>
|
|
#include <QtCore/QMimeData>
|
|
|
|
namespace {
|
|
|
|
constexpr auto kQuestionLimit = 255;
|
|
constexpr auto kMaxOptionsCount = PollData::kMaxOptions;
|
|
constexpr auto kOptionLimit = 100;
|
|
constexpr auto kWarnQuestionLimit = 80;
|
|
constexpr auto kWarnOptionLimit = 30;
|
|
constexpr auto kSolutionLimit = 200;
|
|
constexpr auto kWarnSolutionLimit = 60;
|
|
constexpr auto kErrorLimit = 99;
|
|
constexpr auto kMediaUploadMaxAge = 45 * 60 * crl::time(1000);
|
|
|
|
using PollMediaState = PollMediaUpload::PollMediaState;
|
|
using PollMediaButton = PollMediaUpload::PollMediaButton;
|
|
using PollMediaUploader = PollMediaUpload::PollMediaUploader;
|
|
|
|
using PollMediaUpload::FileListFromMimeData;
|
|
using PollMediaUpload::GenerateDocumentFilePreview;
|
|
using PollMediaUpload::LocalImageThumbnail;
|
|
using PollMediaUpload::PreparePollMediaTask;
|
|
using PollMediaUpload::UploadContext;
|
|
using PollMediaUpload::ValidateFileDragData;
|
|
|
|
class Options {
|
|
public:
|
|
using AttachCallback = Fn<void(
|
|
not_null<Ui::RpWidget*>,
|
|
std::shared_ptr<PollMediaState>)>;
|
|
using FieldDropCallback = Fn<void(
|
|
not_null<Ui::InputField*>,
|
|
std::shared_ptr<PollMediaState>)>;
|
|
using WidgetDropCallback = Fn<void(
|
|
not_null<QWidget*>,
|
|
std::shared_ptr<PollMediaState>)>;
|
|
|
|
Options(
|
|
not_null<Ui::BoxContent*> box,
|
|
not_null<Ui::VerticalLayout*> container,
|
|
not_null<Window::SessionController*> controller,
|
|
ChatHelpers::TabbedPanel *emojiPanel,
|
|
bool chooseCorrectEnabled,
|
|
AttachCallback attachCallback,
|
|
FieldDropCallback fieldDropCallback,
|
|
WidgetDropCallback widgetDropCallback);
|
|
|
|
[[nodiscard]] bool hasOptions() const;
|
|
[[nodiscard]] bool isValid() const;
|
|
[[nodiscard]] bool hasCorrect() const;
|
|
[[nodiscard]] bool hasUploadingMedia() const;
|
|
bool refreshStaleMedia(crl::time threshold);
|
|
[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
|
|
void focusFirst();
|
|
|
|
void enableChooseCorrect(bool enabled, bool multiCorrect = false);
|
|
|
|
[[nodiscard]] not_null<Ui::RpWidget*> layoutWidget() const;
|
|
[[nodiscard]] rpl::producer<int> usedCount() const;
|
|
[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
|
|
[[nodiscard]] rpl::producer<> backspaceInFront() const;
|
|
[[nodiscard]] rpl::producer<> tabbed() const;
|
|
|
|
void handlePaste(
|
|
not_null<Ui::InputField*> field,
|
|
const QStringList &list);
|
|
|
|
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,
|
|
AttachCallback attachCallback,
|
|
FieldDropCallback fieldDropCallback,
|
|
WidgetDropCallback widgetDropCallback);
|
|
|
|
Option(const Option &other) = delete;
|
|
Option &operator=(const Option &other) = delete;
|
|
|
|
void enableChooseCorrect(
|
|
std::shared_ptr<Ui::RadiobuttonGroup> group,
|
|
bool multiCorrect = false,
|
|
Fn<void()> checkboxChanged = nullptr);
|
|
|
|
void show(anim::type animated);
|
|
void destroy(FnMut<void()> done);
|
|
|
|
[[nodiscard]] 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 uploadingMedia() const;
|
|
bool refreshMediaIfStale(crl::time threshold);
|
|
[[nodiscard]] bool hasFocus() const;
|
|
void setFocus() const;
|
|
|
|
void setPlaceholder() const;
|
|
void removePlaceholder() const;
|
|
void showAddIcon(bool show);
|
|
|
|
[[nodiscard]] not_null<Ui::InputField*> field() const;
|
|
[[nodiscard]] not_null<Ui::RpWidget*> wrapWidget() const;
|
|
|
|
[[nodiscard]] PollAnswer toPollAnswer(int index) const;
|
|
|
|
[[nodiscard]] Ui::RpWidget *handleWidget() const;
|
|
|
|
private:
|
|
void createAttach();
|
|
void createWarning();
|
|
void createHandle();
|
|
void updateFieldGeometry();
|
|
|
|
base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap;
|
|
not_null<Ui::RpWidget*> _content;
|
|
base::unique_qptr<Ui::FadeWrapScaled<Ui::Checkbox>> _correct;
|
|
base::unique_qptr<Ui::FadeWrapScaled<Ui::RpWidget>> _handle;
|
|
bool _hasCorrect = false;
|
|
Ui::InputField *_field = nullptr;
|
|
base::unique_qptr<Ui::PlainShadow> _shadow;
|
|
base::unique_qptr<PollMediaButton> _attach;
|
|
AttachCallback _attachCallback;
|
|
FieldDropCallback _fieldDropCallback;
|
|
WidgetDropCallback _widgetDropCallback;
|
|
std::shared_ptr<PollMediaState> _media;
|
|
Ui::FadeWrapScaled<Ui::RpWidget> *_addIcon = nullptr;
|
|
|
|
};
|
|
|
|
[[nodiscard]] bool full() const;
|
|
[[nodiscard]] bool correctShadows() const;
|
|
void fixShadows();
|
|
void removeEmptyTail();
|
|
void addEmptyOption();
|
|
void insertOption(
|
|
int beforeIndex,
|
|
const QString &text,
|
|
anim::type animated);
|
|
void initOptionField(not_null<Ui::InputField*> field);
|
|
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;
|
|
int findLayoutPosition(not_null<Option*> option) const;
|
|
[[nodiscard]] auto createChooseCorrectGroup()
|
|
-> std::shared_ptr<Ui::RadiobuttonGroup>;
|
|
void setupReorder();
|
|
void restartReorder();
|
|
|
|
not_null<Ui::BoxContent*> _box;
|
|
not_null<Ui::VerticalLayout*> _container;
|
|
const not_null<Window::SessionController*> _controller;
|
|
ChatHelpers::TabbedPanel * const _emojiPanel;
|
|
const AttachCallback _attachCallback;
|
|
const FieldDropCallback _fieldDropCallback;
|
|
const WidgetDropCallback _widgetDropCallback;
|
|
std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;
|
|
bool _multiCorrect = false;
|
|
Fn<void()> _multiCorrectChanged;
|
|
Ui::VerticalLayout *_optionsLayout = nullptr;
|
|
std::unique_ptr<Ui::VerticalLayoutReorder> _reorder;
|
|
int _reordering = 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;
|
|
rpl::event_stream<> _tabbed;
|
|
rpl::lifetime _emojiPanelLifetime;
|
|
|
|
};
|
|
|
|
void InitField(
|
|
not_null<QWidget*> container,
|
|
not_null<Ui::InputField*> field,
|
|
not_null<Main::Session*> session,
|
|
std::shared_ptr<Main::SessionShow> show = nullptr,
|
|
base::flat_set<QString> markdownTags = {}) {
|
|
InitMessageFieldHandlers({
|
|
.session = session,
|
|
.show = std::move(show),
|
|
.field = field,
|
|
.allowMarkdownTags = std::move(markdownTags),
|
|
});
|
|
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);
|
|
field->changes(
|
|
) | rpl::on_next([=] {
|
|
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);
|
|
if (value >= 0) {
|
|
result->setText(QString::number(value));
|
|
} else {
|
|
constexpr auto kMinus = QChar(0x2212);
|
|
result->setMarkedText(Ui::Text::Colorized(
|
|
kMinus + QString::number(std::abs(value))));
|
|
}
|
|
result->setVisible(shown);
|
|
}));
|
|
}, field->lifetime());
|
|
return result;
|
|
}
|
|
|
|
void FocusAtEnd(not_null<Ui::InputField*> field) {
|
|
field->setFocus();
|
|
field->setCursorPosition(field->getLastText().size());
|
|
field->ensureCursorVisible();
|
|
}
|
|
|
|
[[nodiscard]] QStringList ParsePastedList(const QString &text) {
|
|
auto list = QStringView(text).split('\n');
|
|
for (auto i = list.begin(); i != list.end();) {
|
|
auto trimmed = i->trimmed();
|
|
if (trimmed.isEmpty() && (i + 1 != list.end())) {
|
|
i = list.erase(i);
|
|
} else {
|
|
*i++ = trimmed;
|
|
}
|
|
}
|
|
if (list.size() < 2) {
|
|
return {};
|
|
}
|
|
auto result = QStringList();
|
|
result.reserve(list.size());
|
|
for (const auto &view : list) {
|
|
result.push_back(view.toString());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
not_null<DetailedSettingsButton*> AddPollToggleButton(
|
|
not_null<Ui::VerticalLayout*> container,
|
|
rpl::producer<QString> title,
|
|
rpl::producer<QString> description,
|
|
Settings::IconDescriptor icon,
|
|
rpl::producer<bool> toggled,
|
|
const style::DetailedSettingsButtonStyle &rowStyle) {
|
|
return AddDetailedSettingsButton(
|
|
container,
|
|
std::move(title),
|
|
std::move(description),
|
|
std::move(icon),
|
|
std::move(toggled),
|
|
rowStyle);
|
|
}
|
|
|
|
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,
|
|
AttachCallback attachCallback,
|
|
FieldDropCallback fieldDropCallback,
|
|
WidgetDropCallback widgetDropCallback)
|
|
: _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::createPollOptionFieldPremium,
|
|
Ui::InputField::Mode::MultiLine,
|
|
tr::lng_polls_create_option_add()))
|
|
, _attachCallback(std::move(attachCallback))
|
|
, _fieldDropCallback(std::move(fieldDropCallback))
|
|
, _widgetDropCallback(std::move(widgetDropCallback))
|
|
, _media(std::make_shared<PollMediaState>()) {
|
|
InitField(outer, _field, session);
|
|
_field->setMaxLength(kOptionLimit + kErrorLimit);
|
|
_field->show();
|
|
if (_fieldDropCallback) {
|
|
_fieldDropCallback(_field, _media);
|
|
}
|
|
|
|
_wrap->hide(anim::type::instant);
|
|
|
|
_content->paintRequest(
|
|
) | rpl::on_next([content = _content.get()] {
|
|
auto p = QPainter(content);
|
|
p.fillRect(content->rect(), st::boxBg);
|
|
}, _content->lifetime());
|
|
|
|
_content->widthValue(
|
|
) | rpl::on_next([=] {
|
|
updateFieldGeometry();
|
|
}, _field->lifetime());
|
|
|
|
_field->heightValue(
|
|
) | rpl::on_next([=](int height) {
|
|
_content->resize(_content->width(), height);
|
|
}, _field->lifetime());
|
|
|
|
_field->changes(
|
|
) | rpl::on_next([=] {
|
|
Ui::PostponeCall(crl::guard(_field, [=] {
|
|
if (_hasCorrect) {
|
|
_correct->toggle(isGood(), anim::type::normal);
|
|
} else if (_handle) {
|
|
_handle->toggle(isGood(), anim::type::normal);
|
|
}
|
|
}));
|
|
}, _field->lifetime());
|
|
|
|
createShadow();
|
|
createAttach();
|
|
createWarning();
|
|
createHandle();
|
|
enableChooseCorrect(group);
|
|
if (_correct) {
|
|
_correct->finishAnimating();
|
|
}
|
|
if (_handle) {
|
|
_handle->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::on_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::createAttach() {
|
|
const auto field = Option::field();
|
|
const auto attach = Ui::CreateChild<PollMediaButton>(
|
|
field.get(),
|
|
st::pollAttach,
|
|
_media);
|
|
attach->show();
|
|
field->sizeValue(
|
|
) | rpl::on_next([=](QSize size) {
|
|
attach->moveToRight(
|
|
st::createPollOptionRemovePosition.x(),
|
|
st::createPollOptionRemovePosition.y() - st::lineWidth * 2,
|
|
size.width());
|
|
}, attach->lifetime());
|
|
attach->clicks(
|
|
) | rpl::on_next([=](Qt::MouseButton button) {
|
|
if (button != Qt::LeftButton) {
|
|
return;
|
|
}
|
|
if (_attachCallback) {
|
|
_attachCallback(not_null<Ui::RpWidget*>(attach), _media);
|
|
}
|
|
}, attach->lifetime());
|
|
if (_widgetDropCallback) {
|
|
_widgetDropCallback(attach, _media);
|
|
}
|
|
_attach.reset(attach);
|
|
}
|
|
|
|
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::on_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());
|
|
}
|
|
|
|
void Options::Option::createHandle() {
|
|
auto widget = object_ptr<Ui::RpWidget>(_content.get());
|
|
const auto raw = widget.data();
|
|
const auto &icon = st::pollBoxMenuPollOrderIcon;
|
|
raw->resize(icon.width(), icon.height());
|
|
raw->setCursor(Qt::SizeVerCursor);
|
|
raw->paintRequest(
|
|
) | rpl::on_next([=] {
|
|
auto p = QPainter(raw);
|
|
icon.paint(p, 0, 0, raw->width());
|
|
}, raw->lifetime());
|
|
|
|
const auto wrap = Ui::CreateChild<Ui::FadeWrapScaled<Ui::RpWidget>>(
|
|
_content.get(),
|
|
std::move(widget));
|
|
wrap->hide(anim::type::instant);
|
|
|
|
_content->sizeValue(
|
|
) | rpl::on_next([=](QSize size) {
|
|
const auto left = st::createPollFieldPadding.left();
|
|
wrap->moveToLeft(
|
|
left,
|
|
(size.height() - wrap->heightNoMargins()) / 2);
|
|
}, wrap->lifetime());
|
|
|
|
_handle.reset(wrap);
|
|
}
|
|
|
|
Ui::RpWidget *Options::Option::handleWidget() const {
|
|
return _handle ? _handle->entity() : nullptr;
|
|
}
|
|
|
|
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::uploadingMedia() const {
|
|
return _media->uploading;
|
|
}
|
|
|
|
bool Options::Option::refreshMediaIfStale(crl::time threshold) {
|
|
if (_media->media
|
|
&& _media->uploadedAt > 0
|
|
&& (!threshold
|
|
|| (crl::now() - _media->uploadedAt > threshold))
|
|
&& _media->reupload) {
|
|
_media->reupload();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Options::Option::hasFocus() const {
|
|
return field()->hasFocus();
|
|
}
|
|
|
|
void Options::Option::setFocus() const {
|
|
FocusAtEnd(field());
|
|
}
|
|
|
|
void Options::Option::setPlaceholder() const {
|
|
field()->setPlaceholder(tr::lng_polls_create_option_add());
|
|
}
|
|
|
|
void Options::Option::enableChooseCorrect(
|
|
std::shared_ptr<Ui::RadiobuttonGroup> group,
|
|
bool multiCorrect,
|
|
Fn<void()> checkboxChanged) {
|
|
if (!group && !multiCorrect) {
|
|
_hasCorrect = false;
|
|
if (_correct) {
|
|
_correct->hide(anim::type::normal);
|
|
}
|
|
if (_handle) {
|
|
_handle->toggle(isGood(), anim::type::normal);
|
|
}
|
|
return;
|
|
}
|
|
static auto Index = 0;
|
|
auto checkbox = multiCorrect
|
|
? object_ptr<Ui::Checkbox>(
|
|
_content.get(),
|
|
QString(),
|
|
false,
|
|
st::defaultCheckbox,
|
|
st::defaultCheck)
|
|
: object_ptr<Ui::Checkbox>(object_ptr<Ui::Radiobutton>(
|
|
_content.get(),
|
|
group,
|
|
++Index,
|
|
QString(),
|
|
st::defaultCheckbox));
|
|
const auto button = Ui::CreateChild<Ui::FadeWrapScaled<Ui::Checkbox>>(
|
|
_content.get(),
|
|
std::move(checkbox));
|
|
button->entity()->resize(
|
|
button->entity()->height(),
|
|
button->entity()->height());
|
|
button->hide(anim::type::instant);
|
|
_content->sizeValue(
|
|
) | rpl::on_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 (multiCorrect && checkboxChanged) {
|
|
button->entity()->checkedChanges(
|
|
) | rpl::on_next([=](bool) {
|
|
checkboxChanged();
|
|
}, button->lifetime());
|
|
}
|
|
if (isGood()) {
|
|
_correct->show(anim::type::normal);
|
|
} else {
|
|
_correct->hide(anim::type::instant);
|
|
}
|
|
if (_handle) {
|
|
_handle->hide(anim::type::normal);
|
|
}
|
|
}
|
|
|
|
void Options::Option::updateFieldGeometry() {
|
|
const auto skip = st::defaultRadio.diameter
|
|
+ st::defaultCheckbox.textPosition.x();
|
|
_field->resizeToWidth(_content->width() - skip);
|
|
_field->moveToLeft(skip, 0);
|
|
}
|
|
|
|
not_null<Ui::InputField*> Options::Option::field() const {
|
|
return _field;
|
|
}
|
|
|
|
not_null<Ui::RpWidget*> Options::Option::wrapWidget() const {
|
|
return _wrap.get();
|
|
}
|
|
|
|
void Options::Option::removePlaceholder() const {
|
|
field()->setPlaceholder(rpl::single(QString()));
|
|
}
|
|
|
|
void Options::Option::showAddIcon(bool show) {
|
|
if (show && !_addIcon) {
|
|
auto icon = Settings::Icon(Settings::IconDescriptor{
|
|
&st::settingsIconAdd,
|
|
Settings::IconType::Round,
|
|
&st::windowBgActive,
|
|
});
|
|
const auto iconSize = icon.size();
|
|
auto widget = object_ptr<Ui::RpWidget>(_content.get());
|
|
const auto raw = widget.data();
|
|
raw->resize(iconSize);
|
|
const auto iconPtr = std::make_shared<Settings::Icon>(
|
|
std::move(icon));
|
|
raw->paintOn([=](QPainter &p) {
|
|
iconPtr->paint(p, 0, 0);
|
|
});
|
|
|
|
const auto wrap =
|
|
Ui::CreateChild<Ui::FadeWrapScaled<Ui::RpWidget>>(
|
|
_content.get(),
|
|
std::move(widget));
|
|
wrap->hide(anim::type::instant);
|
|
|
|
_content->sizeValue(
|
|
) | rpl::on_next([=](QSize size) {
|
|
const auto &handleIcon = st::pollBoxMenuPollOrderIcon;
|
|
const auto left = st::createPollFieldPadding.left()
|
|
+ (handleIcon.width() - iconSize.width()) / 2;
|
|
wrap->moveToLeft(
|
|
left,
|
|
(size.height() - wrap->heightNoMargins()) / 2);
|
|
}, wrap->lifetime());
|
|
|
|
_addIcon = wrap;
|
|
}
|
|
if (_addIcon) {
|
|
if (show) {
|
|
_addIcon->show(anim::type::normal);
|
|
} else {
|
|
_addIcon->hide(anim::type::normal);
|
|
}
|
|
}
|
|
}
|
|
|
|
PollAnswer Options::Option::toPollAnswer(int index) const {
|
|
Expects(index >= 0 && index < kMaxOptionsCount);
|
|
|
|
const auto text = field()->getTextWithAppliedMarkdown();
|
|
|
|
auto result = PollAnswer{
|
|
TextWithEntities{
|
|
.text = text.text,
|
|
.entities = TextUtilities::ConvertTextTagsToEntities(text.tags),
|
|
},
|
|
QByteArray(1, ('0' + index)),
|
|
};
|
|
result.media = _media->media;
|
|
TextUtilities::Trim(result.text);
|
|
result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
|
|
return result;
|
|
}
|
|
|
|
Options::Options(
|
|
not_null<Ui::BoxContent*> box,
|
|
not_null<Ui::VerticalLayout*> container,
|
|
not_null<Window::SessionController*> controller,
|
|
ChatHelpers::TabbedPanel *emojiPanel,
|
|
bool chooseCorrectEnabled,
|
|
AttachCallback attachCallback,
|
|
FieldDropCallback fieldDropCallback,
|
|
WidgetDropCallback widgetDropCallback)
|
|
: _box(box)
|
|
, _container(container)
|
|
, _controller(controller)
|
|
, _emojiPanel(emojiPanel)
|
|
, _attachCallback(std::move(attachCallback))
|
|
, _fieldDropCallback(std::move(fieldDropCallback))
|
|
, _widgetDropCallback(std::move(widgetDropCallback))
|
|
, _chooseCorrectGroup(chooseCorrectEnabled
|
|
? createChooseCorrectGroup()
|
|
: nullptr) {
|
|
auto optionsObj = object_ptr<Ui::VerticalLayout>(container);
|
|
_optionsLayout = optionsObj.data();
|
|
container->add(std::move(optionsObj));
|
|
setupReorder();
|
|
checkLastOption();
|
|
}
|
|
|
|
bool Options::full() const {
|
|
const auto limit = _controller->session().appConfig().pollOptionsLimit();
|
|
return (_list.size() >= limit);
|
|
}
|
|
|
|
bool Options::hasOptions() const {
|
|
return _hasOptions;
|
|
}
|
|
|
|
bool Options::isValid() const {
|
|
return _isValid;
|
|
}
|
|
|
|
bool Options::hasCorrect() const {
|
|
return _hasCorrect;
|
|
}
|
|
|
|
bool Options::hasUploadingMedia() const {
|
|
return ranges::any_of(_list, &Option::uploadingMedia);
|
|
}
|
|
|
|
bool Options::refreshStaleMedia(crl::time threshold) {
|
|
auto refreshed = false;
|
|
for (const auto &option : _list) {
|
|
if (option->refreshMediaIfStale(threshold)) {
|
|
refreshed = true;
|
|
}
|
|
}
|
|
return refreshed;
|
|
}
|
|
|
|
not_null<Ui::RpWidget*> Options::layoutWidget() const {
|
|
return _optionsLayout;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
rpl::producer<> Options::tabbed() const {
|
|
return _tabbed.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::views::filter(&Option::isGood)
|
|
| ranges::views::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, bool multiCorrect) {
|
|
_multiCorrect = enabled && multiCorrect;
|
|
if (_multiCorrect) {
|
|
_chooseCorrectGroup = nullptr;
|
|
_multiCorrectChanged = [=] { validateState(); };
|
|
for (auto &option : _list) {
|
|
option->enableChooseCorrect(
|
|
nullptr,
|
|
true,
|
|
_multiCorrectChanged);
|
|
}
|
|
} else {
|
|
_multiCorrectChanged = nullptr;
|
|
_chooseCorrectGroup = enabled
|
|
? createChooseCorrectGroup()
|
|
: nullptr;
|
|
for (auto &option : _list) {
|
|
option->enableChooseCorrect(_chooseCorrectGroup);
|
|
}
|
|
}
|
|
validateState();
|
|
restartReorder();
|
|
}
|
|
|
|
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::views::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) {
|
|
if (_reorder) {
|
|
_reorder->cancel();
|
|
}
|
|
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)->showAddIcon(true);
|
|
if (last != begin(_list)) {
|
|
(*(last - 1))->setPlaceholder();
|
|
(*(last - 1))->showAddIcon(false);
|
|
}
|
|
fixShadows();
|
|
}
|
|
|
|
void Options::addEmptyOption() {
|
|
if (full()) {
|
|
return;
|
|
} else if (!_list.empty() && _list.back()->isEmpty()) {
|
|
return;
|
|
}
|
|
const auto animated = _list.empty()
|
|
? anim::type::instant
|
|
: anim::type::normal;
|
|
insertOption(int(_list.size()), QString(), animated);
|
|
}
|
|
|
|
void Options::insertOption(
|
|
int beforeIndex,
|
|
const QString &text,
|
|
anim::type animated) {
|
|
if (full()) {
|
|
return;
|
|
}
|
|
Assert(beforeIndex >= 0 && beforeIndex <= int(_list.size()));
|
|
|
|
const auto isAppend = (beforeIndex == int(_list.size()));
|
|
if (isAppend) {
|
|
if (!_list.empty()) {
|
|
_list.back()->showAddIcon(false);
|
|
}
|
|
if (_list.size() > 1) {
|
|
(*(_list.end() - 2))->removePlaceholder();
|
|
}
|
|
}
|
|
|
|
const auto layoutPosition = isAppend
|
|
? _optionsLayout->count()
|
|
: findLayoutPosition(_list[beforeIndex].get());
|
|
|
|
auto option = std::make_unique<Option>(
|
|
_box,
|
|
_optionsLayout,
|
|
&_controller->session(),
|
|
layoutPosition,
|
|
_chooseCorrectGroup,
|
|
_attachCallback,
|
|
_fieldDropCallback,
|
|
_widgetDropCallback);
|
|
const auto raw = option.get();
|
|
_list.insert(begin(_list) + beforeIndex, std::move(option));
|
|
|
|
if (_multiCorrect) {
|
|
raw->enableChooseCorrect(
|
|
nullptr,
|
|
true,
|
|
_multiCorrectChanged);
|
|
}
|
|
if (!text.isEmpty()) {
|
|
raw->field()->setText(text);
|
|
}
|
|
initOptionField(raw->field());
|
|
|
|
if (isAppend) {
|
|
raw->showAddIcon(true);
|
|
}
|
|
raw->show(animated);
|
|
fixShadows();
|
|
restartReorder();
|
|
}
|
|
|
|
void Options::initOptionField(not_null<Ui::InputField*> field) {
|
|
if (const auto emojiPanel = _emojiPanel) {
|
|
const auto isPremium = _controller->session().user()->isPremium();
|
|
const auto emojiToggle = Ui::AddEmojiToggleToField(
|
|
field,
|
|
_box,
|
|
_controller,
|
|
emojiPanel,
|
|
QPoint(
|
|
-st::createPollOptionFieldPremium.textMargins.right(),
|
|
st::createPollOptionEmojiPositionSkip));
|
|
emojiToggle->shownValue() | rpl::on_next([=](bool shown) {
|
|
if (!shown) {
|
|
return;
|
|
}
|
|
_emojiPanelLifetime.destroy();
|
|
emojiPanel->selector()->emojiChosen(
|
|
) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {
|
|
if (field->hasFocus()) {
|
|
Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji);
|
|
}
|
|
}, _emojiPanelLifetime);
|
|
if (isPremium) {
|
|
emojiPanel->selector()->customEmojiChosen(
|
|
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
|
|
if (field->hasFocus()) {
|
|
Data::InsertCustomEmoji(field, data.document);
|
|
}
|
|
}, _emojiPanelLifetime);
|
|
}
|
|
}, emojiToggle->lifetime());
|
|
}
|
|
field->submits(
|
|
) | rpl::on_next([=] {
|
|
const auto index = findField(field);
|
|
if (_list[index]->isGood() && index + 1 < _list.size()) {
|
|
_list[index + 1]->setFocus();
|
|
}
|
|
}, field->lifetime());
|
|
field->changes(
|
|
) | rpl::on_next([=] {
|
|
auto list = ParsePastedList(field->getLastText());
|
|
if (!list.empty()) {
|
|
field->setText(list.front());
|
|
field->forceProcessContentsChanges();
|
|
list.pop_front();
|
|
handlePaste(field, list);
|
|
}
|
|
Ui::PostponeCall(crl::guard(field, [=] {
|
|
validateState();
|
|
}));
|
|
}, field->lifetime());
|
|
field->focusedChanges(
|
|
) | rpl::filter(rpl::mappers::_1) | rpl::on_next([=] {
|
|
_scrollToWidget.fire_copy(field);
|
|
}, field->lifetime());
|
|
field->tabbed(
|
|
) | rpl::on_next([=](not_null<bool*> handled) {
|
|
const auto index = findField(field);
|
|
if (index + 1 < _list.size()) {
|
|
_list[index + 1]->setFocus();
|
|
} else {
|
|
_tabbed.fire({});
|
|
}
|
|
*handled = true;
|
|
}, field->lifetime());
|
|
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;
|
|
});
|
|
}
|
|
|
|
void Options::handlePaste(
|
|
not_null<Ui::InputField*> field,
|
|
const QStringList &list) {
|
|
const auto index = findField(field);
|
|
for (auto i = 0, count = int(list.size()); i != count; ++i) {
|
|
insertOption(
|
|
index + 1 + i,
|
|
list[i],
|
|
anim::type::instant);
|
|
}
|
|
const auto last = std::min(
|
|
int(index + list.size()),
|
|
int(_list.size()) - 1);
|
|
const auto focus = _list[last]->field();
|
|
crl::on_main(focus, [=] {
|
|
focus->setCursorPosition(focus->getLastText().size());
|
|
focus->setFocus();
|
|
});
|
|
}
|
|
|
|
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);
|
|
restartReorder();
|
|
}
|
|
|
|
void Options::validateState() {
|
|
checkLastOption();
|
|
_hasOptions = (ranges::count_if(_list, &Option::isGood) > 0);
|
|
_isValid = _hasOptions && ranges::none_of(_list, &Option::isTooLong);
|
|
_hasCorrect = ranges::any_of(_list, &Option::isCorrect);
|
|
|
|
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;
|
|
}
|
|
|
|
int Options::findLayoutPosition(not_null<Option*> option) const {
|
|
const auto widget = option->wrapWidget();
|
|
for (auto i = 0, count = _optionsLayout->count(); i != count; ++i) {
|
|
if (_optionsLayout->widgetAt(i).get() == widget.get()) {
|
|
return i;
|
|
}
|
|
}
|
|
Unexpected("Poll option widget missing in layout.");
|
|
}
|
|
|
|
void Options::checkLastOption() {
|
|
removeEmptyTail();
|
|
addEmptyOption();
|
|
}
|
|
|
|
void Options::setupReorder() {
|
|
_reorder = std::make_unique<Ui::VerticalLayoutReorder>(
|
|
_optionsLayout);
|
|
_reorder->setMouseEventProxy([=](int i)
|
|
-> not_null<Ui::RpWidget*> {
|
|
if (i < int(_list.size())) {
|
|
if (const auto handle = _list[i]->handleWidget()) {
|
|
return handle;
|
|
}
|
|
}
|
|
return _optionsLayout->widgetAt(i);
|
|
});
|
|
_reorder->updates(
|
|
) | rpl::on_next([=](Ui::VerticalLayoutReorder::Single data) {
|
|
using State = Ui::VerticalLayoutReorder::State;
|
|
if (data.state == State::Started) {
|
|
++_reordering;
|
|
} else {
|
|
Ui::PostponeCall(_optionsLayout, [=] {
|
|
--_reordering;
|
|
});
|
|
if (data.state == State::Applied) {
|
|
base::reorder(
|
|
_list,
|
|
data.oldPosition,
|
|
data.newPosition);
|
|
fixShadows();
|
|
}
|
|
}
|
|
}, _box->lifetime());
|
|
}
|
|
|
|
void Options::restartReorder() {
|
|
if (!_reorder) {
|
|
return;
|
|
}
|
|
_reorder->cancel();
|
|
|
|
if (!_destroyed.empty()) {
|
|
return;
|
|
}
|
|
if (_chooseCorrectGroup || _multiCorrect) {
|
|
return;
|
|
}
|
|
|
|
_reorder->clearPinnedIntervals();
|
|
|
|
const auto count = int(_list.size());
|
|
if (count < 2) {
|
|
return;
|
|
}
|
|
if (_list.back()->isEmpty()) {
|
|
_reorder->addPinnedInterval(count - 1, 1);
|
|
}
|
|
_reorder->start();
|
|
}
|
|
|
|
class DurationIconAction final : public Ui::Menu::Action {
|
|
public:
|
|
DurationIconAction(
|
|
not_null<Ui::Menu::Menu*> parent,
|
|
const style::Menu &st,
|
|
not_null<QAction*> action,
|
|
const QString &tinyText);
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent *e) override;
|
|
|
|
private:
|
|
QString _tinyText;
|
|
|
|
};
|
|
|
|
DurationIconAction::DurationIconAction(
|
|
not_null<Ui::Menu::Menu*> parent,
|
|
const style::Menu &st,
|
|
not_null<QAction*> 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);
|
|
}
|
|
|
|
void ShowMediaUploadingToast() {
|
|
Ui::Toast::Show({
|
|
.title = tr::lng_polls_media_uploading_toast_title(tr::now),
|
|
.text = tr::lng_polls_media_uploading_toast(tr::now, tr::marked),
|
|
.iconLottie = u"uploading"_q,
|
|
.iconLottieSize = st::pollToastUploadingIconSize,
|
|
.duration = crl::time(3000),
|
|
});
|
|
}
|
|
|
|
} // namespace
|
|
|
|
CreatePollBox::CreatePollBox(
|
|
QWidget*,
|
|
not_null<Window::SessionController*> controller,
|
|
not_null<PeerData*> peer,
|
|
PollData::Flags chosen,
|
|
PollData::Flags disabled,
|
|
rpl::producer<int> starsRequired,
|
|
Api::SendType sendType,
|
|
SendMenu::Details sendMenuDetails)
|
|
: _controller(controller)
|
|
, _peer(peer)
|
|
, _chosen(chosen)
|
|
, _disabled(disabled)
|
|
, _sendType(sendType)
|
|
, _sendMenuDetails([result = sendMenuDetails] { return result; })
|
|
, _starsRequired(std::move(starsRequired)) {
|
|
}
|
|
|
|
rpl::producer<CreatePollBox::Result> CreatePollBox::submitRequests() const {
|
|
return _submitRequests.events();
|
|
}
|
|
|
|
void CreatePollBox::setInnerFocus() {
|
|
_setInnerFocus();
|
|
}
|
|
|
|
void CreatePollBox::submitFailed(const QString &error) {
|
|
showToast(error);
|
|
}
|
|
|
|
void CreatePollBox::submitMediaExpired() {
|
|
if (_refreshExpiredMedia) {
|
|
_refreshExpiredMedia();
|
|
ShowMediaUploadingToast();
|
|
}
|
|
}
|
|
|
|
not_null<Ui::InputField*> CreatePollBox::setupQuestion(
|
|
not_null<Ui::VerticalLayout*> container) {
|
|
using namespace Settings;
|
|
|
|
const auto session = &_controller->session();
|
|
const auto isPremium = session->user()->isPremium();
|
|
Ui::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
|
|
+ QMargins(0, 0, st::defaultComposeFiles.emoji.inner.width, 0));
|
|
InitField(
|
|
getDelegate()->outerContainer(),
|
|
question,
|
|
session,
|
|
_controller->uiShow());
|
|
question->setMaxLength(kQuestionLimit + kErrorLimit);
|
|
question->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
|
|
|
|
{
|
|
using Selector = ChatHelpers::TabbedSelector;
|
|
const auto outer = getDelegate()->outerContainer();
|
|
_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
|
|
outer,
|
|
_controller,
|
|
object_ptr<Selector>(
|
|
nullptr,
|
|
_controller->uiShow(),
|
|
Window::GifPauseReason::Layer,
|
|
Selector::Mode::EmojiOnly));
|
|
const auto emojiPanel = _emojiPanel.get();
|
|
emojiPanel->setDesiredHeightValues(
|
|
1.,
|
|
st::emojiPanMinHeight / 2,
|
|
st::emojiPanMinHeight);
|
|
emojiPanel->hide();
|
|
emojiPanel->selector()->setCurrentPeer(session->user());
|
|
|
|
const auto emojiToggle = Ui::AddEmojiToggleToField(
|
|
question,
|
|
this,
|
|
_controller,
|
|
emojiPanel,
|
|
st::createPollOptionFieldPremiumEmojiPosition);
|
|
emojiToggle->show();
|
|
emojiPanel->selector()->emojiChosen(
|
|
) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {
|
|
if (question->hasFocus()) {
|
|
Ui::InsertEmojiAtCursor(question->textCursor(), data.emoji);
|
|
}
|
|
}, emojiToggle->lifetime());
|
|
if (isPremium) {
|
|
emojiPanel->selector()->customEmojiChosen(
|
|
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
|
|
if (question->hasFocus()) {
|
|
Data::InsertCustomEmoji(question, data.document);
|
|
}
|
|
}, emojiToggle->lifetime());
|
|
}
|
|
}
|
|
|
|
const auto warning = CreateWarningLabel(
|
|
container,
|
|
question,
|
|
kQuestionLimit,
|
|
kWarnQuestionLimit);
|
|
rpl::combine(
|
|
question->geometryValue(),
|
|
warning->sizeValue()
|
|
) | rpl::on_next([=](QRect geometry, QSize label) {
|
|
warning->moveToLeft(
|
|
(container->width()
|
|
- label.width()
|
|
- st::createPollWarningPosition.x()),
|
|
(geometry.y()
|
|
- st::createPollFieldPadding.top()
|
|
- st::defaultSubsectionTitlePadding.bottom()
|
|
- st::defaultSubsectionTitle.style.font->height
|
|
+ st::defaultSubsectionTitle.style.font->ascent
|
|
- st::createPollWarning.style.font->ascent),
|
|
geometry.width());
|
|
}, warning->lifetime());
|
|
|
|
return question;
|
|
}
|
|
|
|
not_null<Ui::InputField*> CreatePollBox::setupDescription(
|
|
not_null<Ui::VerticalLayout*> container) {
|
|
const auto session = &_controller->session();
|
|
const auto isPremium = session->user()->isPremium();
|
|
const auto description = container->add(
|
|
object_ptr<Ui::InputField>(
|
|
container,
|
|
st::pollDescriptionField,
|
|
Ui::InputField::Mode::MultiLine,
|
|
tr::lng_polls_create_description_placeholder()),
|
|
st::pollDescriptionFieldPadding);
|
|
InitField(
|
|
getDelegate()->outerContainer(),
|
|
description,
|
|
session,
|
|
_controller->uiShow());
|
|
description->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
|
|
|
|
if (const auto emojiPanel = _emojiPanel.get()) {
|
|
const auto emojiToggle = Ui::AddEmojiToggleToField(
|
|
description,
|
|
this,
|
|
_controller,
|
|
emojiPanel,
|
|
QPoint(
|
|
-st::pollDescriptionField.textMargins.right(),
|
|
-st::lineWidth));
|
|
emojiToggle->shownValue() | rpl::on_next([=](bool shown) {
|
|
if (!shown) {
|
|
return;
|
|
}
|
|
emojiPanel->selector()->emojiChosen(
|
|
) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {
|
|
if (description->hasFocus()) {
|
|
Ui::InsertEmojiAtCursor(
|
|
description->textCursor(),
|
|
data.emoji);
|
|
}
|
|
}, emojiToggle->lifetime());
|
|
if (isPremium) {
|
|
emojiPanel->selector()->customEmojiChosen(
|
|
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
|
|
if (description->hasFocus()) {
|
|
Data::InsertCustomEmoji(description, data.document);
|
|
}
|
|
}, emojiToggle->lifetime());
|
|
}
|
|
}, emojiToggle->lifetime());
|
|
}
|
|
|
|
return description;
|
|
}
|
|
|
|
not_null<Ui::InputField*> CreatePollBox::setupSolution(
|
|
not_null<Ui::VerticalLayout*> container,
|
|
rpl::producer<bool> shown) {
|
|
using namespace Settings;
|
|
|
|
const auto outer = container->add(
|
|
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
|
container,
|
|
object_ptr<Ui::VerticalLayout>(container))
|
|
)->toggleOn(std::move(shown));
|
|
const auto inner = outer->entity();
|
|
|
|
const auto session = &_controller->session();
|
|
Ui::AddSkip(inner);
|
|
Ui::AddSubsectionTitle(inner, tr::lng_polls_solution_title());
|
|
const auto solution = inner->add(
|
|
object_ptr<Ui::InputField>(
|
|
inner,
|
|
st::pollMediaField,
|
|
Ui::InputField::Mode::MultiLine,
|
|
tr::lng_polls_solution_placeholder()),
|
|
st::createPollFieldPadding);
|
|
InitField(
|
|
getDelegate()->outerContainer(),
|
|
solution,
|
|
session,
|
|
_controller->uiShow(),
|
|
{
|
|
Ui::InputField::kTagBold,
|
|
Ui::InputField::kTagItalic,
|
|
Ui::InputField::kTagUnderline,
|
|
Ui::InputField::kTagStrikeOut,
|
|
Ui::InputField::kTagCode,
|
|
Ui::InputField::kTagSpoiler,
|
|
});
|
|
solution->setMaxLength(kSolutionLimit + kErrorLimit);
|
|
|
|
const auto warning = CreateWarningLabel(
|
|
inner,
|
|
solution,
|
|
kSolutionLimit,
|
|
kWarnSolutionLimit);
|
|
rpl::combine(
|
|
solution->geometryValue(),
|
|
warning->sizeValue()
|
|
) | rpl::on_next([=](QRect geometry, QSize label) {
|
|
warning->moveToLeft(
|
|
(inner->width()
|
|
- label.width()
|
|
- st::createPollWarningPosition.x()),
|
|
(geometry.y()
|
|
- st::createPollFieldPadding.top()
|
|
- st::defaultSubsectionTitlePadding.bottom()
|
|
- st::defaultSubsectionTitle.style.font->height
|
|
+ st::defaultSubsectionTitle.style.font->ascent
|
|
- st::createPollWarning.style.font->ascent),
|
|
geometry.width());
|
|
}, warning->lifetime());
|
|
|
|
Ui::AddDividerText(
|
|
inner,
|
|
tr::lng_polls_solution_about());
|
|
|
|
return solution;
|
|
}
|
|
|
|
object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|
using namespace Settings;
|
|
|
|
const auto id = base::RandomValue<uint64>();
|
|
struct UploadContext {
|
|
std::weak_ptr<PollMediaState> media;
|
|
uint64 token = 0;
|
|
QString filename;
|
|
QString filemime;
|
|
QVector<MTPDocumentAttribute> attributes;
|
|
bool forceFile = true;
|
|
};
|
|
using StartUploadFn = Fn<void(
|
|
std::shared_ptr<PollMediaState>,
|
|
Ui::PreparedFile)>;
|
|
struct State final {
|
|
Errors error = Error::Question;
|
|
std::unique_ptr<Options> options;
|
|
rpl::event_stream<bool> multipleForceOff;
|
|
rpl::event_stream<bool> addOptionsForceOff;
|
|
rpl::event_stream<bool> revotingForceOff;
|
|
rpl::event_stream<bool> quizForceOff;
|
|
rpl::event_stream<bool> showWhoVotedForceOn;
|
|
rpl::variable<int> closePeriod = 0;
|
|
rpl::variable<TimeId> closeDate = TimeId(0);
|
|
rpl::variable<std::vector<QString>> countriesValue;
|
|
std::shared_ptr<PollMediaState> descriptionMedia
|
|
= std::make_shared<PollMediaState>();
|
|
std::shared_ptr<PollMediaState> solutionMedia
|
|
= std::make_shared<PollMediaState>();
|
|
std::weak_ptr<PollMediaState> stickerTarget;
|
|
base::flat_map<FullMsgId, UploadContext> uploads;
|
|
base::unique_qptr<Ui::PopupMenu> mediaMenu;
|
|
base::unique_qptr<Ui::PopupMenu> durationMenu;
|
|
base::unique_qptr<ChatHelpers::TabbedPanel> stickerPanel;
|
|
std::unique_ptr<TaskQueue> prepareQueue;
|
|
StartUploadFn startPhotoUpload;
|
|
StartUploadFn startDocumentUpload;
|
|
StartUploadFn startVideoUpload;
|
|
};
|
|
const auto state = lifetime().make_state<State>();
|
|
state->prepareQueue = std::make_unique<TaskQueue>();
|
|
|
|
auto result = object_ptr<Ui::VerticalLayout>(this);
|
|
const auto container = result.data();
|
|
|
|
const auto updateMedia = [=](
|
|
const std::shared_ptr<PollMediaState> &media) {
|
|
if (media->update) {
|
|
media->update();
|
|
}
|
|
};
|
|
const auto setMedia = [=](
|
|
const std::shared_ptr<PollMediaState> &media,
|
|
PollMedia value,
|
|
std::shared_ptr<Ui::DynamicImage> thumbnail,
|
|
bool rounded) {
|
|
const auto wasUploading = media->uploading;
|
|
media->token++;
|
|
media->media = value;
|
|
media->thumbnail = std::move(thumbnail);
|
|
media->rounded = rounded;
|
|
media->progress = (media->uploading && media->media)
|
|
? 1.
|
|
: 0.;
|
|
media->uploadDataId = 0;
|
|
media->uploading = false;
|
|
if (wasUploading && value) {
|
|
media->uploadedAt = crl::now();
|
|
} else {
|
|
media->uploadedAt = 0;
|
|
media->reupload = nullptr;
|
|
}
|
|
updateMedia(media);
|
|
};
|
|
struct UploadedMedia final {
|
|
PollMedia input;
|
|
std::shared_ptr<Ui::DynamicImage> thumbnail;
|
|
};
|
|
const auto parseUploaded = [=](
|
|
const MTPMessageMedia &result,
|
|
FullMsgId fullId) {
|
|
auto parsed = UploadedMedia();
|
|
auto &owner = _controller->session().data();
|
|
result.match([&](const MTPDmessageMediaPhoto &media) {
|
|
if (const auto photo = media.vphoto()) {
|
|
photo->match([&](const MTPDphoto &) {
|
|
parsed.input.photo = owner.processPhoto(*photo);
|
|
parsed.thumbnail = Ui::MakePhotoThumbnail(
|
|
parsed.input.photo,
|
|
fullId);
|
|
}, [](const auto &) {
|
|
});
|
|
}
|
|
}, [&](const MTPDmessageMediaDocument &media) {
|
|
if (const auto document = media.vdocument()) {
|
|
document->match([&](const MTPDdocument &) {
|
|
parsed.input.document = owner.processDocument(
|
|
*document);
|
|
parsed.thumbnail
|
|
= Ui::MakeDocumentFilePreviewThumbnail(
|
|
parsed.input.document,
|
|
fullId);
|
|
}, [](const auto &) {
|
|
});
|
|
}
|
|
}, [](const auto &) {
|
|
});
|
|
return parsed;
|
|
};
|
|
const auto applyUploaded = [=](
|
|
const std::shared_ptr<PollMediaState> &media,
|
|
uint64 token,
|
|
FullMsgId fullId,
|
|
const MTPInputFile &file) {
|
|
const auto uploaded = MTP_inputMediaUploadedPhoto(
|
|
MTP_flags(0),
|
|
file,
|
|
MTP_vector<MTPInputDocument>(QVector<MTPInputDocument>()),
|
|
MTPint(),
|
|
MTPInputDocument());
|
|
_controller->session().api().request(MTPmessages_UploadMedia(
|
|
MTP_flags(0),
|
|
MTPstring(),
|
|
_peer->input(),
|
|
uploaded
|
|
)).done([=](const MTPMessageMedia &result) {
|
|
if (media->token != token) {
|
|
return;
|
|
}
|
|
auto parsed = parseUploaded(result, fullId);
|
|
if (!parsed.input) {
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
return;
|
|
}
|
|
setMedia(
|
|
media,
|
|
parsed.input,
|
|
media->thumbnail
|
|
? media->thumbnail
|
|
: std::move(parsed.thumbnail),
|
|
true);
|
|
}).fail([=](const MTP::Error &) {
|
|
if (media->token != token) {
|
|
return;
|
|
}
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
}).send();
|
|
};
|
|
const auto applyUploadedDocument = [=](
|
|
const std::shared_ptr<PollMediaState> &media,
|
|
uint64 token,
|
|
FullMsgId fullId,
|
|
const Api::RemoteFileInfo &info,
|
|
const UploadContext &context) {
|
|
using Flag = MTPDinputMediaUploadedDocument::Flag;
|
|
const auto flags = (context.forceFile ? Flag::f_force_file : Flag())
|
|
| (info.thumb ? Flag::f_thumb : Flag());
|
|
auto attributes = !context.attributes.isEmpty()
|
|
? context.attributes
|
|
: QVector<MTPDocumentAttribute>{
|
|
MTP_documentAttributeFilename(
|
|
MTP_string(context.filename)),
|
|
};
|
|
const auto uploaded = MTP_inputMediaUploadedDocument(
|
|
MTP_flags(flags),
|
|
info.file,
|
|
info.thumb.value_or(MTPInputFile()),
|
|
MTP_string(context.filemime),
|
|
MTP_vector<MTPDocumentAttribute>(std::move(attributes)),
|
|
MTP_vector<MTPInputDocument>(),
|
|
MTPInputPhoto(),
|
|
MTP_int(0),
|
|
MTP_int(0));
|
|
_controller->session().api().request(MTPmessages_UploadMedia(
|
|
MTP_flags(0),
|
|
MTPstring(),
|
|
_peer->input(),
|
|
uploaded
|
|
)).done([=](const MTPMessageMedia &result) {
|
|
if (media->token != token) {
|
|
return;
|
|
}
|
|
auto parsed = parseUploaded(result, fullId);
|
|
if (!parsed.input) {
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
return;
|
|
}
|
|
const auto isVideo = parsed.input.document
|
|
&& parsed.input.document->isVideoFile();
|
|
setMedia(
|
|
media,
|
|
parsed.input,
|
|
isVideo
|
|
? (media->thumbnail
|
|
? media->thumbnail
|
|
: std::move(parsed.thumbnail))
|
|
: std::move(parsed.thumbnail),
|
|
isVideo);
|
|
}).fail([=](const MTP::Error &) {
|
|
if (media->token != token) {
|
|
return;
|
|
}
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
}).send();
|
|
};
|
|
_controller->session().uploader().photoReady(
|
|
) | rpl::on_next([=](const Storage::UploadedMedia &data) {
|
|
const auto context = state->uploads.take(data.fullId);
|
|
if (!context) {
|
|
return;
|
|
}
|
|
const auto media = context->media.lock();
|
|
if (!media || (media->token != context->token)) {
|
|
return;
|
|
}
|
|
applyUploaded(media, context->token, data.fullId, data.info.file);
|
|
}, lifetime());
|
|
_controller->session().uploader().photoProgress(
|
|
) | rpl::on_next([=](const FullMsgId &id) {
|
|
const auto i = state->uploads.find(id);
|
|
if (i == state->uploads.end()) {
|
|
return;
|
|
}
|
|
const auto &context = i->second;
|
|
const auto media = context.media.lock();
|
|
if (!media
|
|
|| (media->token != context.token)
|
|
|| !media->uploadDataId) {
|
|
return;
|
|
}
|
|
media->progress = _controller->session().data().photo(
|
|
media->uploadDataId)->progress();
|
|
updateMedia(media);
|
|
}, lifetime());
|
|
_controller->session().uploader().photoFailed(
|
|
) | rpl::on_next([=](const FullMsgId &id) {
|
|
const auto context = state->uploads.take(id);
|
|
if (!context) {
|
|
return;
|
|
}
|
|
const auto media = context->media.lock();
|
|
if (!media || (media->token != context->token)) {
|
|
return;
|
|
}
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
}, lifetime());
|
|
_controller->session().uploader().documentReady(
|
|
) | rpl::on_next([=](const Storage::UploadedMedia &data) {
|
|
const auto context = state->uploads.take(data.fullId);
|
|
if (!context) {
|
|
return;
|
|
}
|
|
const auto media = context->media.lock();
|
|
if (!media || (media->token != context->token)) {
|
|
return;
|
|
}
|
|
applyUploadedDocument(
|
|
media,
|
|
context->token,
|
|
data.fullId,
|
|
data.info,
|
|
*context);
|
|
}, lifetime());
|
|
_controller->session().uploader().documentProgress(
|
|
) | rpl::on_next([=](const FullMsgId &id) {
|
|
const auto i = state->uploads.find(id);
|
|
if (i == state->uploads.end()) {
|
|
return;
|
|
}
|
|
const auto &context = i->second;
|
|
const auto media = context.media.lock();
|
|
if (!media
|
|
|| (media->token != context.token)
|
|
|| !media->uploadDataId) {
|
|
return;
|
|
}
|
|
media->progress = _controller->session().data().document(
|
|
media->uploadDataId)->progress();
|
|
updateMedia(media);
|
|
}, lifetime());
|
|
_controller->session().uploader().documentFailed(
|
|
) | rpl::on_next([=](const FullMsgId &id) {
|
|
const auto context = state->uploads.take(id);
|
|
if (!context) {
|
|
return;
|
|
}
|
|
const auto media = context->media.lock();
|
|
if (!media || (media->token != context->token)) {
|
|
return;
|
|
}
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
}, lifetime());
|
|
const auto emojiPaused = [=] {
|
|
using namespace Window;
|
|
return _controller->isGifPausedAtLeastFor(GifPauseReason::Any);
|
|
};
|
|
const auto updateStickerPanelGeometry = [=] {
|
|
if (!state->stickerPanel) {
|
|
return;
|
|
}
|
|
const auto panel = state->stickerPanel.get();
|
|
const auto parent = panel->parentWidget();
|
|
const auto left = std::max(
|
|
(parent->width() - panel->width()) / 2,
|
|
0);
|
|
const auto top = std::max(
|
|
(parent->height() - panel->height()) / 2,
|
|
0);
|
|
panel->moveTopRight(top, left + panel->width());
|
|
};
|
|
const auto showStickerPanel = [=](
|
|
not_null<Ui::RpWidget*>,
|
|
std::shared_ptr<PollMediaState> media) {
|
|
if (!state->stickerPanel) {
|
|
const auto body = getDelegate()->outerContainer();
|
|
state->stickerPanel = HistoryView::CreatePollStickerPanel(
|
|
body,
|
|
_controller);
|
|
state->stickerPanel->setDropDown(true);
|
|
base::install_event_filter(
|
|
body,
|
|
[=](not_null<QEvent*> event) {
|
|
const auto type = event->type();
|
|
if (type == QEvent::Move || type == QEvent::Resize) {
|
|
crl::on_main(this, updateStickerPanelGeometry);
|
|
}
|
|
return base::EventFilterResult::Continue;
|
|
},
|
|
lifetime());
|
|
state->stickerPanel->selector()->fileChosen(
|
|
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
|
|
if (Window::ShowSendPremiumError(
|
|
_controller,
|
|
data.document)) {
|
|
return;
|
|
}
|
|
const auto target = state->stickerTarget.lock();
|
|
if (!target) {
|
|
return;
|
|
}
|
|
setMedia(
|
|
target,
|
|
PollMedia{ .document = data.document },
|
|
Ui::MakeEmojiThumbnail(
|
|
&_controller->session().data(),
|
|
Data::SerializeCustomEmojiId(data.document),
|
|
emojiPaused),
|
|
false);
|
|
state->stickerPanel->hideAnimated();
|
|
}, state->stickerPanel->lifetime());
|
|
}
|
|
state->stickerTarget = media;
|
|
const auto panel = state->stickerPanel.get();
|
|
updateStickerPanelGeometry();
|
|
panel->toggleAnimated();
|
|
};
|
|
const auto asyncReupload = [=](
|
|
std::shared_ptr<PollMediaState> media,
|
|
Fn<Ui::PreparedList()> prepare,
|
|
Fn<bool(const Ui::PreparedList&)> validate,
|
|
const QString &name,
|
|
const StartUploadFn &startUpload) {
|
|
const auto reuploadToken = ++media->token;
|
|
media->media = PollMedia();
|
|
media->uploading = true;
|
|
media->progress = 0.;
|
|
media->uploadDataId = 0;
|
|
updateMedia(media);
|
|
const auto weak = QPointer<CreatePollBox>(this);
|
|
crl::async([=, prepare = std::move(prepare)] {
|
|
auto list = prepare();
|
|
crl::on_main([=, list = std::move(list)]() mutable {
|
|
if (!weak || media->token != reuploadToken) {
|
|
return;
|
|
}
|
|
if (list.error != Ui::PreparedList::Error::None
|
|
|| list.files.empty()
|
|
|| (validate && !validate(list))) {
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
return;
|
|
}
|
|
auto &f = list.files.front();
|
|
if (!name.isEmpty()) {
|
|
f.displayName = name;
|
|
}
|
|
startUpload(media, std::move(f));
|
|
});
|
|
});
|
|
};
|
|
const auto setFileReupload = [=](
|
|
std::shared_ptr<PollMediaState> media,
|
|
const QString &path,
|
|
const QString &name,
|
|
const StartUploadFn &startUpload) {
|
|
media->reupload = crl::guard(this, [=,
|
|
weak = std::weak_ptr(media)] {
|
|
const auto strong = weak.lock();
|
|
if (!strong) {
|
|
return;
|
|
}
|
|
if (path.isEmpty()) {
|
|
setMedia(strong, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
return;
|
|
}
|
|
const auto premium = _controller->session().premium();
|
|
asyncReupload(
|
|
strong,
|
|
[=] {
|
|
return Storage::PrepareMediaList(
|
|
QStringList{ path },
|
|
st::sendMediaPreviewSize,
|
|
premium);
|
|
},
|
|
nullptr,
|
|
name,
|
|
startUpload);
|
|
});
|
|
};
|
|
const auto startPreparedPhotoUpload = [=](
|
|
std::shared_ptr<PollMediaState> media,
|
|
Ui::PreparedFile file) {
|
|
const auto sourceImage = (file.path.isEmpty()
|
|
&& file.content.isEmpty()
|
|
&& file.information)
|
|
? [&]() -> QImage {
|
|
const auto img = std::get_if<
|
|
Ui::PreparedFileInformation::Image>(
|
|
&file.information->media);
|
|
return (img && !img->data.isNull())
|
|
? img->data
|
|
: QImage();
|
|
}()
|
|
: QImage();
|
|
media->reupload = crl::guard(this, [=,
|
|
weak = std::weak_ptr(media),
|
|
path = file.path,
|
|
content = file.content,
|
|
name = file.displayName] {
|
|
const auto strong = weak.lock();
|
|
if (!strong) {
|
|
return;
|
|
}
|
|
const auto premium = _controller->session().premium();
|
|
asyncReupload(
|
|
strong,
|
|
[=] {
|
|
if (!path.isEmpty()) {
|
|
return Storage::PrepareMediaList(
|
|
QStringList{ path },
|
|
st::sendMediaPreviewSize,
|
|
premium);
|
|
}
|
|
if (!content.isEmpty()) {
|
|
auto image = QImage::fromData(content);
|
|
if (!image.isNull()) {
|
|
return Storage::PrepareMediaFromImage(
|
|
std::move(image),
|
|
QByteArray(content),
|
|
st::sendMediaPreviewSize);
|
|
}
|
|
} else if (!sourceImage.isNull()) {
|
|
auto bytes = QByteArray();
|
|
auto buffer = QBuffer(&bytes);
|
|
buffer.open(QIODevice::WriteOnly);
|
|
sourceImage.save(&buffer, "PNG");
|
|
return Storage::PrepareMediaFromImage(
|
|
QImage(sourceImage),
|
|
std::move(bytes),
|
|
st::sendMediaPreviewSize);
|
|
}
|
|
return Ui::PreparedList(
|
|
Ui::PreparedList::Error::EmptyFile,
|
|
QString());
|
|
},
|
|
[](const Ui::PreparedList &list) {
|
|
return list.files.front().type
|
|
== Ui::PreparedFile::Type::Photo;
|
|
},
|
|
name,
|
|
state->startPhotoUpload);
|
|
});
|
|
const auto token = ++media->token;
|
|
media->media = PollMedia();
|
|
media->thumbnail = std::make_shared<LocalImageThumbnail>(
|
|
std::move(file.preview));
|
|
media->rounded = true;
|
|
media->uploading = true;
|
|
media->progress = 0.;
|
|
media->uploadDataId = 0;
|
|
updateMedia(media);
|
|
using PreparePoll = PreparePollMediaTask;
|
|
state->prepareQueue->addTask(std::make_unique<PreparePoll>(
|
|
FileLoadTask::Args{
|
|
.session = &_controller->session(),
|
|
.filepath = file.path,
|
|
.content = file.content,
|
|
.information = std::move(file.information),
|
|
.videoCover = nullptr,
|
|
.type = SendMediaType::Photo,
|
|
.to = FileLoadTo(
|
|
_peer->id,
|
|
Api::SendOptions(),
|
|
FullReplyTo(),
|
|
MsgId()),
|
|
.caption = TextWithTags(),
|
|
.spoiler = false,
|
|
.album = nullptr,
|
|
.forceFile = false,
|
|
.idOverride = 0,
|
|
.displayName = file.displayName,
|
|
},
|
|
[=](std::shared_ptr<FilePrepareResult> prepared) {
|
|
if ((media->token != token)
|
|
|| !prepared
|
|
|| (prepared->type != SendMediaType::Photo)) {
|
|
if (media->token == token) {
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
}
|
|
return;
|
|
}
|
|
const auto uploadId = FullMsgId(
|
|
_peer->id,
|
|
_controller->session().data().nextLocalMessageId());
|
|
state->uploads.emplace(uploadId, UploadContext{
|
|
.media = media,
|
|
.token = token,
|
|
});
|
|
media->uploadDataId = prepared->id;
|
|
_controller->session().uploader().upload(
|
|
uploadId,
|
|
prepared);
|
|
}));
|
|
};
|
|
const auto startPreparedDocumentUpload = [=](
|
|
std::shared_ptr<PollMediaState> media,
|
|
Ui::PreparedFile file) {
|
|
const auto displayName = file.displayName.isEmpty()
|
|
? QFileInfo(file.path).fileName()
|
|
: file.displayName;
|
|
setFileReupload(
|
|
media,
|
|
file.path,
|
|
displayName,
|
|
state->startDocumentUpload);
|
|
auto audioAttributes = PollMediaUpload::ExtractAudioAttributes(file);
|
|
const auto isAudio = !audioAttributes.isEmpty();
|
|
const auto token = ++media->token;
|
|
media->media = PollMedia();
|
|
media->thumbnail = std::make_shared<LocalImageThumbnail>(
|
|
GenerateDocumentFilePreview(
|
|
displayName,
|
|
st::pollAttach.rippleAreaSize));
|
|
media->rounded = false;
|
|
media->uploading = true;
|
|
media->progress = 0.;
|
|
media->uploadDataId = 0;
|
|
updateMedia(media);
|
|
using PreparePoll = PreparePollMediaTask;
|
|
state->prepareQueue->addTask(std::make_unique<PreparePoll>(
|
|
FileLoadTask::Args{
|
|
.session = &_controller->session(),
|
|
.filepath = file.path,
|
|
.content = file.content,
|
|
.information = std::move(file.information),
|
|
.videoCover = nullptr,
|
|
.type = SendMediaType::File,
|
|
.to = FileLoadTo(
|
|
_peer->id,
|
|
Api::SendOptions(),
|
|
FullReplyTo(),
|
|
MsgId()),
|
|
.caption = TextWithTags(),
|
|
.spoiler = false,
|
|
.album = nullptr,
|
|
.forceFile = !isAudio,
|
|
.idOverride = 0,
|
|
.displayName = displayName,
|
|
},
|
|
[=, attributes = std::move(audioAttributes)](
|
|
std::shared_ptr<FilePrepareResult> prepared) {
|
|
if ((media->token != token) || !prepared) {
|
|
if (media->token == token) {
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
}
|
|
return;
|
|
}
|
|
const auto uploadId = FullMsgId(
|
|
_peer->id,
|
|
_controller->session().data().nextLocalMessageId());
|
|
state->uploads.emplace(uploadId, UploadContext{
|
|
.media = media,
|
|
.token = token,
|
|
.filename = prepared->filename,
|
|
.filemime = prepared->filemime,
|
|
.attributes = attributes,
|
|
.forceFile = !isAudio,
|
|
});
|
|
media->uploadDataId = prepared->id;
|
|
_controller->session().uploader().upload(
|
|
uploadId,
|
|
prepared);
|
|
}));
|
|
};
|
|
const auto startPreparedVideoUpload = [=](
|
|
std::shared_ptr<PollMediaState> media,
|
|
Ui::PreparedFile file) {
|
|
setFileReupload(
|
|
media,
|
|
file.path,
|
|
file.displayName,
|
|
state->startVideoUpload);
|
|
const auto token = ++media->token;
|
|
media->media = PollMedia();
|
|
media->thumbnail = std::make_shared<LocalImageThumbnail>(
|
|
std::move(file.preview));
|
|
media->rounded = true;
|
|
media->uploading = true;
|
|
media->progress = 0.;
|
|
media->uploadDataId = 0;
|
|
updateMedia(media);
|
|
using PreparePoll = PreparePollMediaTask;
|
|
state->prepareQueue->addTask(std::make_unique<PreparePoll>(
|
|
FileLoadTask::Args{
|
|
.session = &_controller->session(),
|
|
.filepath = file.path,
|
|
.content = file.content,
|
|
.information = std::move(file.information),
|
|
.videoCover = nullptr,
|
|
.type = SendMediaType::File,
|
|
.to = FileLoadTo(
|
|
_peer->id,
|
|
Api::SendOptions(),
|
|
FullReplyTo(),
|
|
MsgId()),
|
|
.caption = TextWithTags(),
|
|
.spoiler = false,
|
|
.album = nullptr,
|
|
.forceFile = false,
|
|
.idOverride = 0,
|
|
.displayName = file.displayName,
|
|
},
|
|
[=](std::shared_ptr<FilePrepareResult> prepared) {
|
|
if ((media->token != token) || !prepared) {
|
|
if (media->token == token) {
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
showToast(tr::lng_attach_failed(tr::now));
|
|
}
|
|
return;
|
|
}
|
|
auto attributes = QVector<MTPDocumentAttribute>();
|
|
prepared->document.match([&](const MTPDdocument &data) {
|
|
attributes = data.vattributes().v;
|
|
}, [](const auto &) {
|
|
});
|
|
const auto uploadId = FullMsgId(
|
|
_peer->id,
|
|
_controller->session().data().nextLocalMessageId());
|
|
state->uploads.emplace(uploadId, UploadContext{
|
|
.media = media,
|
|
.token = token,
|
|
.filename = prepared->filename,
|
|
.filemime = prepared->filemime,
|
|
.attributes = std::move(attributes),
|
|
.forceFile = false,
|
|
});
|
|
media->uploadDataId = prepared->id;
|
|
_controller->session().uploader().upload(
|
|
uploadId,
|
|
prepared);
|
|
}));
|
|
};
|
|
state->startPhotoUpload = startPreparedPhotoUpload;
|
|
state->startDocumentUpload = startPreparedDocumentUpload;
|
|
state->startVideoUpload = startPreparedVideoUpload;
|
|
const auto applyPreparedPhotoList = [=](
|
|
std::shared_ptr<PollMediaState> media,
|
|
Ui::PreparedList &&list) {
|
|
if (list.error != Ui::PreparedList::Error::None
|
|
|| (list.files.size() != 1)
|
|
|| (list.files.front().type != Ui::PreparedFile::Type::Photo)) {
|
|
return false;
|
|
}
|
|
startPreparedPhotoUpload(media, std::move(list.files.front()));
|
|
return true;
|
|
};
|
|
using ValidateFn = Fn<bool(not_null<const QMimeData*>)>;
|
|
using ApplyDropFn = Fn<bool(
|
|
std::shared_ptr<PollMediaState>,
|
|
not_null<const QMimeData*>)>;
|
|
const auto installDropToWidget = [=](
|
|
not_null<QWidget*> widget,
|
|
std::shared_ptr<PollMediaState> media,
|
|
ValidateFn validate,
|
|
ApplyDropFn apply) {
|
|
widget->setAcceptDrops(true);
|
|
base::install_event_filter(widget, [=](not_null<QEvent*> event) {
|
|
const auto type = event->type();
|
|
if (type != QEvent::DragEnter
|
|
&& type != QEvent::DragMove
|
|
&& type != QEvent::Drop) {
|
|
return base::EventFilterResult::Continue;
|
|
}
|
|
const auto drop = static_cast<QDropEvent*>(event.get());
|
|
const auto data = drop->mimeData();
|
|
if (!data || !validate(data)) {
|
|
return base::EventFilterResult::Continue;
|
|
}
|
|
if (type == QEvent::Drop && !apply(media, data)) {
|
|
return base::EventFilterResult::Continue;
|
|
}
|
|
drop->acceptProposedAction();
|
|
return base::EventFilterResult::Cancel;
|
|
});
|
|
};
|
|
const auto installDropToField = [=](
|
|
not_null<Ui::InputField*> field,
|
|
std::shared_ptr<PollMediaState> media,
|
|
ValidateFn validate,
|
|
ApplyDropFn apply) {
|
|
field->setMimeDataHook([=](
|
|
not_null<const QMimeData*> data,
|
|
Ui::InputField::MimeAction action) {
|
|
if (action == Ui::InputField::MimeAction::Check) {
|
|
return validate(data);
|
|
} else if (action == Ui::InputField::MimeAction::Insert) {
|
|
return apply(media, data);
|
|
}
|
|
Unexpected("Polls: action in MimeData hook.");
|
|
});
|
|
};
|
|
const auto applyPhotoOrVideoDrop = ApplyDropFn([=](
|
|
std::shared_ptr<PollMediaState> media,
|
|
not_null<const QMimeData*> data) {
|
|
auto list = FileListFromMimeData(
|
|
data,
|
|
_controller->session().premium());
|
|
if (list.error != Ui::PreparedList::Error::None
|
|
|| list.files.empty()) {
|
|
return false;
|
|
}
|
|
auto &file = list.files.front();
|
|
if (file.type == Ui::PreparedFile::Type::Photo) {
|
|
startPreparedPhotoUpload(media, std::move(file));
|
|
return true;
|
|
} else if (file.type == Ui::PreparedFile::Type::Video) {
|
|
startPreparedVideoUpload(media, std::move(file));
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
const auto validatePhotoOrVideo = ValidateFn([](
|
|
not_null<const QMimeData*> data) {
|
|
if (data->hasImage()) {
|
|
return true;
|
|
}
|
|
const auto urls = Core::ReadMimeUrls(data);
|
|
if (urls.size() != 1 || !urls.front().isLocalFile()) {
|
|
return false;
|
|
}
|
|
const auto file = Platform::File::UrlToLocal(urls.front());
|
|
const auto mime = Core::MimeTypeForFile(QFileInfo(file)).name();
|
|
return Core::IsMimeAcceptedForPhotoVideoAlbum(mime);
|
|
});
|
|
const auto installPhotoDropToWidget = [=](
|
|
not_null<QWidget*> widget,
|
|
std::shared_ptr<PollMediaState> media) {
|
|
installDropToWidget(
|
|
widget,
|
|
media,
|
|
validatePhotoOrVideo,
|
|
applyPhotoOrVideoDrop);
|
|
};
|
|
const auto installPhotoDropToField = [=](
|
|
not_null<Ui::InputField*> field,
|
|
std::shared_ptr<PollMediaState> media) {
|
|
field->setMimeDataHook([=](
|
|
not_null<const QMimeData*> data,
|
|
Ui::InputField::MimeAction action) {
|
|
using MimeAction = Ui::InputField::MimeAction;
|
|
const auto text = data->hasText()
|
|
? data->text()
|
|
: QString();
|
|
if (text.contains('\n')) {
|
|
if (action == MimeAction::Check) {
|
|
return true;
|
|
}
|
|
auto list = ParsePastedList(text);
|
|
if (list.empty()) {
|
|
return false;
|
|
}
|
|
field->setText(list.front());
|
|
field->forceProcessContentsChanges();
|
|
list.pop_front();
|
|
if (state->options) {
|
|
state->options->handlePaste(field, list);
|
|
}
|
|
return true;
|
|
}
|
|
if (action == MimeAction::Check) {
|
|
return validatePhotoOrVideo(data);
|
|
} else if (action == MimeAction::Insert) {
|
|
return applyPhotoOrVideoDrop(media, data);
|
|
}
|
|
Unexpected("Polls: action in MimeData hook.");
|
|
});
|
|
};
|
|
const auto applyFileDrop = ApplyDropFn([=](
|
|
std::shared_ptr<PollMediaState> media,
|
|
not_null<const QMimeData*> data) {
|
|
auto list = FileListFromMimeData(
|
|
data,
|
|
_controller->session().premium());
|
|
if (list.error == Ui::PreparedList::Error::TooLargeFile) {
|
|
const auto fileSize = list.files.empty()
|
|
? 0
|
|
: list.files.front().size;
|
|
_controller->show(Box(
|
|
FileSizeLimitBox,
|
|
&_controller->session(),
|
|
fileSize,
|
|
nullptr));
|
|
return false;
|
|
}
|
|
if (list.error != Ui::PreparedList::Error::None
|
|
|| list.files.empty()) {
|
|
return false;
|
|
}
|
|
auto &file = list.files.front();
|
|
if (file.type == Ui::PreparedFile::Type::Photo) {
|
|
startPreparedPhotoUpload(media, std::move(file));
|
|
} else if (file.type == Ui::PreparedFile::Type::Video) {
|
|
startPreparedVideoUpload(media, std::move(file));
|
|
} else {
|
|
startPreparedDocumentUpload(media, std::move(file));
|
|
}
|
|
return true;
|
|
});
|
|
const auto validateFile = ValidateFn(ValidateFileDragData);
|
|
const auto choosePhotoOrVideo = [=](
|
|
std::shared_ptr<PollMediaState> media) {
|
|
const auto callback = crl::guard(this, [=](
|
|
FileDialog::OpenResult &&result) {
|
|
const auto checkResult = [&](const Ui::PreparedList &list) {
|
|
using namespace Ui;
|
|
if (list.files.size() != 1) {
|
|
return false;
|
|
}
|
|
const auto type = list.files.front().type;
|
|
return (type == PreparedFile::Type::Photo)
|
|
|| (type == PreparedFile::Type::Video);
|
|
};
|
|
const auto showError = [=](tr::phrase<> text) {
|
|
showToast(text(tr::now));
|
|
};
|
|
auto list = Storage::PreparedFileFromFilesDialog(
|
|
std::move(result),
|
|
checkResult,
|
|
showError,
|
|
st::sendMediaPreviewSize,
|
|
_controller->session().premium());
|
|
if (!list) {
|
|
return;
|
|
}
|
|
auto &file = list->files.front();
|
|
if (file.type == Ui::PreparedFile::Type::Photo) {
|
|
applyPreparedPhotoList(media, std::move(*list));
|
|
} else {
|
|
startPreparedVideoUpload(media, std::move(file));
|
|
}
|
|
});
|
|
FileDialog::GetOpenPath(
|
|
this,
|
|
tr::lng_attach_photo_or_video(tr::now),
|
|
FileDialog::PhotoVideoFilesFilter(),
|
|
callback);
|
|
};
|
|
const auto chooseDocument = [=](std::shared_ptr<PollMediaState> media) {
|
|
const auto callback = crl::guard(this, [=](
|
|
FileDialog::OpenResult &&result) {
|
|
if (result.paths.isEmpty()) {
|
|
return;
|
|
}
|
|
auto list = Storage::PrepareMediaList(
|
|
result.paths.mid(0, 1),
|
|
st::sendMediaPreviewSize,
|
|
_controller->session().premium());
|
|
if (list.error == Ui::PreparedList::Error::TooLargeFile) {
|
|
const auto fileSize = list.files.empty()
|
|
? 0
|
|
: list.files.front().size;
|
|
_controller->show(Box(
|
|
FileSizeLimitBox,
|
|
&_controller->session(),
|
|
fileSize,
|
|
nullptr));
|
|
return;
|
|
} else if (list.error != Ui::PreparedList::Error::None
|
|
|| list.files.empty()) {
|
|
return;
|
|
}
|
|
startPreparedDocumentUpload(
|
|
media,
|
|
std::move(list.files.front()));
|
|
});
|
|
FileDialog::GetOpenPath(
|
|
this,
|
|
tr::lng_attach_file(tr::now),
|
|
FileDialog::AllFilesFilter(),
|
|
callback);
|
|
};
|
|
const auto clearMedia = [=](std::shared_ptr<PollMediaState> media) {
|
|
auto toCancel = std::vector<FullMsgId>();
|
|
for (auto i = state->uploads.begin(); i != state->uploads.end();) {
|
|
if (i->second.media.lock() == media) {
|
|
toCancel.push_back(i->first);
|
|
i = state->uploads.erase(i);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
for (const auto &id : toCancel) {
|
|
_controller->session().uploader().cancel(id);
|
|
}
|
|
setMedia(media, PollMedia(), nullptr, false);
|
|
};
|
|
const auto chooseLocation = [=](
|
|
std::shared_ptr<PollMediaState> media) {
|
|
const auto session = &_controller->session();
|
|
const auto &appConfig = session->appConfig();
|
|
auto map = appConfig.get<base::flat_map<QString, QString>>(
|
|
u"tdesktop_config_map"_q,
|
|
base::flat_map<QString, QString>());
|
|
const auto config = Ui::LocationPickerConfig{
|
|
.mapsToken = map[u"maps"_q],
|
|
.geoToken = map[u"geo"_q],
|
|
};
|
|
const auto applyGeo = [=](float64 lat, float64 lon) {
|
|
const auto point = Data::LocationPoint(
|
|
lat,
|
|
lon,
|
|
Data::LocationPoint::NoAccessHash);
|
|
auto pollMedia = PollMedia();
|
|
pollMedia.geo = point;
|
|
const auto cloudImage = session->data().location(point);
|
|
auto thumbnail = Ui::MakeGeoThumbnailWithPin(
|
|
cloudImage,
|
|
session,
|
|
Data::FileOrigin());
|
|
setMedia(media, pollMedia, std::move(thumbnail), true);
|
|
};
|
|
if (base::IsCtrlPressed()) {
|
|
const auto lat = 48.8566 + base::RandomValue<uint32>()
|
|
/ float64(std::numeric_limits<uint32>::max()) * 0.02 - 0.01;
|
|
const auto lon = 2.3522 + base::RandomValue<uint32>()
|
|
/ float64(std::numeric_limits<uint32>::max()) * 0.02 - 0.01;
|
|
applyGeo(lat, lon);
|
|
return;
|
|
}
|
|
if (!Ui::LocationPicker::Available(config)) {
|
|
return;
|
|
}
|
|
Ui::LocationPicker::Show({
|
|
.parent = _controller->widget().get(),
|
|
.config = config,
|
|
.chooseLabel = tr::lng_maps_point_send(),
|
|
.session = session,
|
|
.callback = crl::guard(this, [=](Data::InputVenue venue) {
|
|
applyGeo(venue.lat, venue.lon);
|
|
}),
|
|
.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
|
|
.storageId = session->local().resolveStorageIdBots(),
|
|
.closeRequests = _controller->content()->death(),
|
|
});
|
|
};
|
|
const auto showMediaMenu = [=](
|
|
not_null<Ui::RpWidget*> button,
|
|
std::shared_ptr<PollMediaState> media,
|
|
bool allowDocuments = false,
|
|
bool allowStickers = true) {
|
|
if (HistoryView::ShowPollMediaPreview(_controller, media, {
|
|
.choosePhotoOrVideo = [=] { choosePhotoOrVideo(media); },
|
|
.chooseDocument = [=] { chooseDocument(media); },
|
|
.chooseSticker = [=] {
|
|
showStickerPanel(button, media);
|
|
},
|
|
.editPhoto = crl::guard(this, [=](Ui::PreparedList list) {
|
|
applyPreparedPhotoList(media, std::move(list));
|
|
}),
|
|
.remove = [=] { clearMedia(media); },
|
|
})) {
|
|
return;
|
|
}
|
|
state->mediaMenu = base::make_unique_q<Ui::PopupMenu>(
|
|
button,
|
|
st::popupMenuWithIcons);
|
|
state->mediaMenu->setForcedOrigin(
|
|
Ui::PanelAnimation::Origin::TopRight);
|
|
state->mediaMenu->addAction(
|
|
tr::lng_attach_photo_or_video(tr::now),
|
|
[=] { choosePhotoOrVideo(media); },
|
|
&st::menuIconPhoto);
|
|
if (allowDocuments) {
|
|
state->mediaMenu->addAction(
|
|
tr::lng_attach_file(tr::now),
|
|
[=] { chooseDocument(media); },
|
|
&st::menuIconFile);
|
|
}
|
|
{
|
|
const auto &appConfig = _controller->session().appConfig();
|
|
auto map = appConfig.get<base::flat_map<QString, QString>>(
|
|
u"tdesktop_config_map"_q,
|
|
base::flat_map<QString, QString>());
|
|
const auto config = Ui::LocationPickerConfig{
|
|
.mapsToken = map[u"maps"_q],
|
|
.geoToken = map[u"geo"_q],
|
|
};
|
|
if (Ui::LocationPicker::Available(config)) {
|
|
state->mediaMenu->addAction(
|
|
tr::lng_maps_point(tr::now),
|
|
[=] { chooseLocation(media); },
|
|
&st::menuIconAddress);
|
|
}
|
|
}
|
|
if (allowStickers) {
|
|
state->mediaMenu->addAction(
|
|
tr::lng_chat_intro_choose_sticker(tr::now),
|
|
[=] { showStickerPanel(button, media); },
|
|
&st::menuIconStickers);
|
|
}
|
|
if (media->media || media->uploading) {
|
|
state->mediaMenu->addAction(
|
|
tr::lng_box_remove(tr::now),
|
|
[=] { clearMedia(media); },
|
|
&st::menuIconDelete);
|
|
}
|
|
state->mediaMenu->popup(QCursor::pos());
|
|
};
|
|
const auto addMediaButton = [=](
|
|
not_null<Ui::InputField*> field,
|
|
std::shared_ptr<PollMediaState> media) {
|
|
const auto button = Ui::CreateChild<PollMediaButton>(
|
|
field,
|
|
st::pollAttach,
|
|
media);
|
|
button->show();
|
|
installDropToField(field, media, validateFile, applyFileDrop);
|
|
installDropToWidget(button, media, validateFile, applyFileDrop);
|
|
field->sizeValue(
|
|
) | rpl::on_next([=](QSize size) {
|
|
button->moveToRight(
|
|
st::createPollAttachPosition.x(),
|
|
st::createPollAttachPosition.y(),
|
|
size.width());
|
|
}, button->lifetime());
|
|
button->clicks(
|
|
) | rpl::on_next([=](Qt::MouseButton buttonType) {
|
|
if (buttonType != Qt::LeftButton) {
|
|
return;
|
|
}
|
|
showMediaMenu(button, media, true, false);
|
|
}, button->lifetime());
|
|
};
|
|
|
|
const auto question = setupQuestion(container);
|
|
const auto description = setupDescription(container);
|
|
addMediaButton(description, state->descriptionMedia);
|
|
Ui::AddDivider(container);
|
|
Ui::AddSkip(container);
|
|
container->add(
|
|
object_ptr<Ui::FlatLabel>(
|
|
container,
|
|
tr::lng_polls_create_options(),
|
|
st::defaultSubsectionTitle),
|
|
st::createPollFieldTitlePadding);
|
|
state->options = std::make_unique<Options>(
|
|
this,
|
|
container,
|
|
_controller,
|
|
_emojiPanel ? _emojiPanel.get() : nullptr,
|
|
(_chosen & PollData::Flag::Quiz),
|
|
showMediaMenu,
|
|
installPhotoDropToField,
|
|
installPhotoDropToWidget);
|
|
const auto options = state->options.get();
|
|
auto limit = options->usedCount() | rpl::after_next([=](int count) {
|
|
setCloseByEscape(!count);
|
|
setCloseByOutsideClick(!count);
|
|
}) | rpl::map([=](int count) {
|
|
const auto appConfig = &_controller->session().appConfig();
|
|
const auto max = appConfig->pollOptionsLimit();
|
|
return (count < max)
|
|
? tr::lng_polls_create_limit(tr::now, lt_count, max - 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));
|
|
|
|
question->tabbed(
|
|
) | rpl::on_next([=](not_null<bool*> handled) {
|
|
description->setFocus();
|
|
*handled = true;
|
|
}, question->lifetime());
|
|
|
|
description->tabbed(
|
|
) | rpl::on_next([=](not_null<bool*> handled) {
|
|
options->focusFirst();
|
|
*handled = true;
|
|
}, description->lifetime());
|
|
|
|
Ui::AddSkip(container);
|
|
Ui::AddSubsectionTitle(container, tr::lng_polls_create_settings());
|
|
const auto isBroadcastChannel = _peer->isChannel()
|
|
&& !_peer->isMegagroup();
|
|
|
|
const auto showWhoVoted = (!(_disabled & PollData::Flag::PublicVotes))
|
|
? AddPollToggleButton(
|
|
container,
|
|
tr::lng_polls_create_show_who_voted(),
|
|
tr::lng_polls_create_show_who_voted_about(),
|
|
{
|
|
.icon = &st::pollBoxFilledPollViewIcon,
|
|
.background = &st::settingsIconBg4,
|
|
},
|
|
rpl::single(!!(_chosen & PollData::Flag::PublicVotes))
|
|
| rpl::then(state->showWhoVotedForceOn.events()),
|
|
st::detailedSettingsButtonStyle).get()
|
|
: nullptr;
|
|
const auto multiple = AddPollToggleButton(
|
|
container,
|
|
tr::lng_polls_create_allow_multiple_answers(),
|
|
tr::lng_polls_create_allow_multiple_answers_about(),
|
|
{
|
|
.icon = &st::pollBoxFilledPollMultipleIcon,
|
|
.background = &st::settingsIconBg3,
|
|
},
|
|
rpl::single(!!(_chosen & PollData::Flag::MultiChoice))
|
|
| rpl::then(state->multipleForceOff.events()),
|
|
st::detailedSettingsButtonStyle);
|
|
const auto addOptions = (!(_disabled & PollData::Flag::OpenAnswers))
|
|
? AddPollToggleButton(
|
|
container,
|
|
tr::lng_polls_create_allow_adding_options(),
|
|
tr::lng_polls_create_allow_adding_options_about(),
|
|
{
|
|
.icon = &st::pollBoxFilledPollAddIcon,
|
|
.background = &st::settingsIconBg4,
|
|
},
|
|
rpl::single(!!(_chosen & PollData::Flag::OpenAnswers))
|
|
| rpl::then(state->addOptionsForceOff.events()),
|
|
st::detailedSettingsButtonStyle).get()
|
|
: nullptr;
|
|
const auto revoting = AddPollToggleButton(
|
|
container,
|
|
tr::lng_polls_create_allow_revoting(),
|
|
tr::lng_polls_create_allow_revoting_about(),
|
|
{
|
|
.icon = &st::pollBoxFilledPollRevoteIcon,
|
|
.background = &st::settingsIconBg6,
|
|
},
|
|
rpl::single(!(_chosen & PollData::Flag::RevotingDisabled))
|
|
| rpl::then(state->revotingForceOff.events()),
|
|
st::detailedSettingsButtonStyle);
|
|
const auto shuffle = AddPollToggleButton(
|
|
container,
|
|
tr::lng_polls_create_shuffle_options(),
|
|
tr::lng_polls_create_shuffle_options_about(),
|
|
{
|
|
.icon = &st::pollBoxFilledPollShuffleIcon,
|
|
.background = &st::settingsIconBg8,
|
|
},
|
|
rpl::single(!!(_chosen & PollData::Flag::ShuffleAnswers)),
|
|
st::detailedSettingsButtonStyle);
|
|
const auto quiz = AddPollToggleButton(
|
|
container,
|
|
tr::lng_polls_create_set_correct_answer(),
|
|
rpl::single(multiple->toggled()) | rpl::then(
|
|
multiple->toggledChanges()
|
|
) | rpl::map([](bool multi) {
|
|
return multi
|
|
? tr::lng_polls_create_set_correct_answer_about_multi(
|
|
tr::now)
|
|
: tr::lng_polls_create_set_correct_answer_about(tr::now);
|
|
}),
|
|
{
|
|
.icon = &st::pollBoxFilledPollCorrectIcon,
|
|
.background = &st::settingsIconBg2,
|
|
},
|
|
rpl::single(!!(_chosen & PollData::Flag::Quiz))
|
|
| rpl::then(state->quizForceOff.events()),
|
|
st::detailedSettingsButtonStyle);
|
|
|
|
const auto show = uiShow();
|
|
|
|
const auto restrictToSubscribers = isBroadcastChannel
|
|
? AddPollToggleButton(
|
|
container,
|
|
tr::lng_polls_create_restrict_to_subscribers(),
|
|
tr::lng_polls_create_restrict_to_subscribers_about(),
|
|
{
|
|
.icon = &st::pollBoxFilledPollSubscribersIcon,
|
|
.background = &st::settingsIconBg5,
|
|
},
|
|
rpl::single(!!(_chosen & PollData::Flag::SubscribersOnly)),
|
|
st::detailedSettingsButtonStyle).get()
|
|
: nullptr;
|
|
const auto limitByCountry = isBroadcastChannel
|
|
? AddPollToggleButton(
|
|
container,
|
|
tr::lng_polls_create_limit_by_country(),
|
|
tr::lng_polls_create_limit_by_country_about(),
|
|
{
|
|
.icon = &st::pollBoxFilledPollCountryIcon,
|
|
.background = &st::settingsIconBg4,
|
|
},
|
|
rpl::single(false),
|
|
st::detailedSettingsButtonStyle).get()
|
|
: nullptr;
|
|
const auto countriesWrap = limitByCountry
|
|
? container->add(
|
|
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
|
container,
|
|
object_ptr<Ui::VerticalLayout>(container)))
|
|
: nullptr;
|
|
const auto countriesButton = [=] {
|
|
if (!countriesWrap) {
|
|
return (Ui::SettingsButton*)(nullptr);
|
|
}
|
|
const auto inner = countriesWrap->entity();
|
|
return AddButtonWithLabel(
|
|
inner,
|
|
tr::lng_polls_create_allowed_countries(),
|
|
state->countriesValue.value(
|
|
) | rpl::map([=](const std::vector<QString> &countries) {
|
|
if (countries.empty()) {
|
|
return QString();
|
|
}
|
|
if (countries.size() == 1) {
|
|
return Countries::Instance().countryNameByISO2(
|
|
countries.front(),
|
|
Countries::Naming::Polls);
|
|
}
|
|
return tr::lng_polls_create_countries_count(
|
|
tr::now,
|
|
lt_count,
|
|
countries.size());
|
|
}),
|
|
st::settingsButtonNoIcon).get();
|
|
}();
|
|
if (countriesWrap) {
|
|
countriesWrap->toggleOn(
|
|
rpl::single(limitByCountry->toggled())
|
|
| rpl::then(limitByCountry->toggledChanges()));
|
|
}
|
|
if (countriesButton) {
|
|
countriesButton->setClickedCallback([=] {
|
|
const auto done = [=](std::vector<QString> countries) {
|
|
state->countriesValue = std::move(countries);
|
|
};
|
|
const auto limit
|
|
= _controller->session().appConfig().pollCountriesMax();
|
|
const auto checkError = [=](int count) {
|
|
if (count >= limit) {
|
|
show->showToast(tr::lng_polls_create_countries_limit(
|
|
tr::now,
|
|
lt_count,
|
|
limit));
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
show->show(Box(
|
|
Ui::SelectCountriesBox,
|
|
state->countriesValue.current(),
|
|
done,
|
|
checkError,
|
|
Countries::Naming::Polls));
|
|
});
|
|
}
|
|
|
|
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<Ui::SlideWrap<Ui::VerticalLayout>>(
|
|
container,
|
|
object_ptr<Ui::VerticalLayout>(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);
|
|
|
|
pollEndsLabel->setClickedCallback([=] {
|
|
state->durationMenu = base::make_unique_q<Ui::PopupMenu>(
|
|
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<DurationIconAction>(
|
|
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<Ui::GenericBox*> 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<Ui::SettingsButton>(
|
|
durationInner,
|
|
tr::lng_polls_create_hide_results(),
|
|
st::settingsButtonNoIcon)
|
|
)->toggleOn(rpl::single(false));
|
|
|
|
const auto solution = setupSolution(
|
|
container,
|
|
rpl::single(quiz->toggled()) | rpl::then(quiz->toggledChanges()));
|
|
addMediaButton(solution, state->solutionMedia);
|
|
|
|
options->tabbed(
|
|
) | rpl::on_next([=] {
|
|
if (quiz->toggled()) {
|
|
solution->setFocus();
|
|
} else {
|
|
question->setFocus();
|
|
}
|
|
}, question->lifetime());
|
|
|
|
solution->tabbed(
|
|
) | rpl::on_next([=](not_null<bool*> handled) {
|
|
question->setFocus();
|
|
*handled = true;
|
|
}, solution->lifetime());
|
|
|
|
const auto updateAddOptionsLocked = [=] {
|
|
if (addOptions) {
|
|
const auto locked = (_disabled & PollData::Flag::OpenAnswers)
|
|
|| quiz->toggled()
|
|
|| (showWhoVoted && !showWhoVoted->toggled());
|
|
addOptions->setToggleLocked(locked);
|
|
if (locked) {
|
|
state->addOptionsForceOff.fire(false);
|
|
}
|
|
}
|
|
};
|
|
const auto updateQuizDependentLocks = [=](bool checked) {
|
|
updateAddOptionsLocked();
|
|
revoting->setToggleLocked(
|
|
_disabled & PollData::Flag::RevotingDisabled);
|
|
};
|
|
quiz->setToggleLocked(_disabled & PollData::Flag::Quiz);
|
|
shuffle->setToggleLocked(_disabled & PollData::Flag::ShuffleAnswers);
|
|
if (restrictToSubscribers) {
|
|
restrictToSubscribers->setToggleLocked(
|
|
_disabled & PollData::Flag::SubscribersOnly);
|
|
}
|
|
updateQuizDependentLocks(quiz->toggled());
|
|
|
|
using namespace rpl::mappers;
|
|
quiz->toggledChanges(
|
|
) | rpl::on_next([=](bool checked) {
|
|
if (checked && (_disabled & PollData::Flag::Quiz)) {
|
|
state->quizForceOff.fire(false);
|
|
return;
|
|
}
|
|
if (checked) {
|
|
state->addOptionsForceOff.fire(false);
|
|
state->revotingForceOff.fire(false);
|
|
solution->setFocus();
|
|
}
|
|
updateQuizDependentLocks(checked);
|
|
options->enableChooseCorrect(checked, multiple->toggled());
|
|
}, quiz->lifetime());
|
|
|
|
multiple->toggledChanges(
|
|
) | rpl::on_next([=](bool checked) {
|
|
if (quiz->toggled()) {
|
|
options->enableChooseCorrect(true, checked);
|
|
}
|
|
}, multiple->lifetime());
|
|
|
|
if (addOptions && showWhoVoted) {
|
|
updateAddOptionsLocked();
|
|
showWhoVoted->toggledChanges(
|
|
) | rpl::on_next([=](bool) {
|
|
updateAddOptionsLocked();
|
|
}, showWhoVoted->lifetime());
|
|
}
|
|
|
|
const auto isValidQuestion = [=] {
|
|
const auto text = question->getLastText().trimmed();
|
|
return !text.isEmpty() && (text.size() <= kQuestionLimit);
|
|
};
|
|
question->submits(
|
|
) | rpl::on_next([=] {
|
|
if (isValidQuestion()) {
|
|
description->setFocus();
|
|
}
|
|
}, question->lifetime());
|
|
|
|
description->submits(
|
|
) | rpl::on_next([=] {
|
|
options->focusFirst();
|
|
}, description->lifetime());
|
|
|
|
_setInnerFocus = [=] {
|
|
question->setFocusFast();
|
|
};
|
|
|
|
const auto collectResult = [=] {
|
|
const auto textWithTags = question->getTextWithAppliedMarkdown();
|
|
const auto descriptionWithTags = description->getTextWithTags();
|
|
using Flag = PollData::Flag;
|
|
auto result = PollData(&_controller->session().data(), id);
|
|
result.question.text = textWithTags.text;
|
|
result.question.entities = TextUtilities::ConvertTextTagsToEntities(
|
|
textWithTags.tags);
|
|
TextUtilities::Trim(result.question);
|
|
result.answers = options->toPollAnswers();
|
|
const auto solutionWithTags = quiz->toggled()
|
|
? solution->getTextWithAppliedMarkdown()
|
|
: TextWithTags();
|
|
result.solution = TextWithEntities{
|
|
solutionWithTags.text,
|
|
TextUtilities::ConvertTextTagsToEntities(solutionWithTags.tags)
|
|
};
|
|
result.attachedMedia = state->descriptionMedia->media;
|
|
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 subscribersOnly = (restrictToSubscribers
|
|
&& restrictToSubscribers->toggled());
|
|
const auto hideResultsEnabled = duration->toggled()
|
|
&& hideResults->toggled();
|
|
result.countries = (limitByCountry
|
|
&& limitByCountry->toggled())
|
|
? state->countriesValue.current()
|
|
: std::vector<QString>();
|
|
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))
|
|
| (subscribersOnly ? Flag::SubscribersOnly : Flag(0))
|
|
| (hideResultsEnabled
|
|
? Flag::HideResultsUntilClose
|
|
: Flag(0)));
|
|
auto text = TextWithEntities{
|
|
descriptionWithTags.text,
|
|
TextUtilities::ConvertTextTagsToEntities(
|
|
descriptionWithTags.tags),
|
|
};
|
|
TextUtilities::Trim(text);
|
|
return Result{
|
|
std::move(result),
|
|
std::move(text),
|
|
Api::SendOptions(),
|
|
};
|
|
};
|
|
const auto collectError = [=] {
|
|
if (isValidQuestion()) {
|
|
state->error &= ~Error::Question;
|
|
} else {
|
|
state->error |= Error::Question;
|
|
}
|
|
if (!options->hasOptions()) {
|
|
state->error |= Error::Options;
|
|
} else if (!options->isValid()) {
|
|
state->error |= Error::Other;
|
|
} else {
|
|
state->error &= ~(Error::Options | Error::Other);
|
|
}
|
|
if (quiz->toggled() && !options->hasCorrect()) {
|
|
state->error |= Error::Correct;
|
|
} else {
|
|
state->error &= ~Error::Correct;
|
|
}
|
|
if (quiz->toggled()
|
|
&& solution->getLastText().trimmed().size() > kSolutionLimit) {
|
|
state->error |= Error::Solution;
|
|
} else {
|
|
state->error &= ~Error::Solution;
|
|
}
|
|
if (state->descriptionMedia->uploading
|
|
|| (quiz->toggled() && state->solutionMedia->uploading)
|
|
|| options->hasUploadingMedia()) {
|
|
state->error |= Error::Media;
|
|
} 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;
|
|
}
|
|
if (limitByCountry
|
|
&& limitByCountry->toggled()
|
|
&& state->countriesValue.current().empty()) {
|
|
state->error |= Error::Country;
|
|
} else {
|
|
state->error &= ~Error::Country;
|
|
}
|
|
};
|
|
const auto showError = [show = uiShow()](
|
|
tr::phrase<> text) {
|
|
show->showToast(text(tr::now));
|
|
};
|
|
_refreshExpiredMedia = [=] {
|
|
const auto forceRefresh = [](
|
|
const std::shared_ptr<PollMediaState> &m) {
|
|
if (m->media && m->reupload) {
|
|
m->reupload();
|
|
}
|
|
};
|
|
forceRefresh(state->descriptionMedia);
|
|
forceRefresh(state->solutionMedia);
|
|
options->refreshStaleMedia(0);
|
|
};
|
|
const auto send = [=](Api::SendOptions sendOptions) {
|
|
const auto kStaleTimeout = kMediaUploadMaxAge;
|
|
auto refreshedAny = false;
|
|
const auto tryRefresh = [&](
|
|
const std::shared_ptr<PollMediaState> &m) {
|
|
if (m->media
|
|
&& m->uploadedAt > 0
|
|
&& (crl::now() - m->uploadedAt > kStaleTimeout)
|
|
&& m->reupload) {
|
|
m->reupload();
|
|
refreshedAny = true;
|
|
}
|
|
};
|
|
tryRefresh(state->descriptionMedia);
|
|
if (quiz->toggled()) {
|
|
tryRefresh(state->solutionMedia);
|
|
}
|
|
if (options->refreshStaleMedia(kStaleTimeout)) {
|
|
refreshedAny = true;
|
|
}
|
|
if (refreshedAny) {
|
|
collectError();
|
|
if (state->error & Error::Media) {
|
|
ShowMediaUploadingToast();
|
|
}
|
|
return;
|
|
}
|
|
collectError();
|
|
if (state->error & Error::Question) {
|
|
showError(tr::lng_polls_choose_question);
|
|
question->setFocus();
|
|
} else if (state->error & Error::Options) {
|
|
showError(tr::lng_polls_choose_answers);
|
|
options->focusFirst();
|
|
} else if (state->error & Error::Correct) {
|
|
showError(tr::lng_polls_choose_correct);
|
|
scrollToWidget(options->layoutWidget());
|
|
} else if (state->error & Error::Solution) {
|
|
solution->showError();
|
|
} else if (state->error & Error::Media) {
|
|
ShowMediaUploadingToast();
|
|
} else if (state->error & Error::Deadline) {
|
|
showError(tr::lng_polls_create_deadline_expired);
|
|
} else if (state->error & Error::Country) {
|
|
showError(tr::lng_polls_create_choose_country);
|
|
if (countriesButton) {
|
|
scrollToWidget(countriesButton);
|
|
}
|
|
} else if (!state->error) {
|
|
auto result = collectResult();
|
|
result.options = sendOptions;
|
|
_submitRequests.fire(std::move(result));
|
|
}
|
|
};
|
|
const auto sendAction = SendMenu::DefaultCallback(
|
|
_controller->uiShow(),
|
|
crl::guard(this, send));
|
|
|
|
options->scrollToWidget(
|
|
) | rpl::on_next([=](not_null<QWidget*> widget) {
|
|
scrollToWidget(widget);
|
|
}, lifetime());
|
|
|
|
options->backspaceInFront(
|
|
) | rpl::on_next([=] {
|
|
FocusAtEnd(description);
|
|
}, lifetime());
|
|
|
|
const auto isNormal = (_sendType == Api::SendType::Normal);
|
|
const auto schedule = [=] {
|
|
sendAction(
|
|
{ .type = SendMenu::ActionType::Schedule },
|
|
_sendMenuDetails());
|
|
};
|
|
const auto submit = addButton(
|
|
tr::lng_polls_create_button(),
|
|
[=] { isNormal ? send({}) : schedule(); });
|
|
submit->setText(PaidSendButtonText(_starsRequired.value(), isNormal
|
|
? tr::lng_polls_create_button()
|
|
: tr::lng_schedule_button()));
|
|
const auto sendMenuDetails = [=] {
|
|
collectError();
|
|
return (state->error) ? SendMenu::Details() : _sendMenuDetails();
|
|
};
|
|
SendMenu::SetupMenuAndShortcuts(
|
|
submit.data(),
|
|
_controller->uiShow(),
|
|
sendMenuDetails,
|
|
sendAction);
|
|
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
|
|
|
if (showWhoVoted) {
|
|
showWhoVoted->finishAnimating();
|
|
}
|
|
multiple->finishAnimating();
|
|
if (addOptions) {
|
|
addOptions->finishAnimating();
|
|
}
|
|
revoting->finishAnimating();
|
|
shuffle->finishAnimating();
|
|
quiz->finishAnimating();
|
|
duration->finishAnimating();
|
|
durationWrap->finishAnimating();
|
|
hideResults->finishAnimating();
|
|
if (restrictToSubscribers) {
|
|
restrictToSubscribers->finishAnimating();
|
|
}
|
|
if (limitByCountry) {
|
|
limitByCountry->finishAnimating();
|
|
}
|
|
if (countriesWrap) {
|
|
countriesWrap->finishAnimating();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void CreatePollBox::prepare() {
|
|
setTitle(tr::lng_polls_create_title());
|
|
|
|
const auto inner = setInnerWidget(setupContent());
|
|
|
|
setDimensionsToContent(st::boxWideWidth, inner);
|
|
|
|
Ui::SetStickyBottomScroll(this, inner);
|
|
}
|