mirror of https://github.com/procxx/kepka.git
Add create poll box from groups three-dot menu.
This commit is contained in:
parent
74c1db740d
commit
b6f7832745
Telegram
|
@ -1836,6 +1836,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_polls_stop" = "Stop poll";
|
||||
"lng_polls_stop_warning" = "If you stop this poll now, nobody will be able to vote in it anymore. This action cannot be undone.";
|
||||
"lng_polls_stop_sure" = "Stop";
|
||||
"lng_polls_create" = "Create poll";
|
||||
"lng_polls_create_title" = "New poll";
|
||||
"lng_polls_create_question" = "Question";
|
||||
"lng_polls_create_question_placeholder" = "Ask a question";
|
||||
"lng_polls_create_options" = "Poll options";
|
||||
"lng_polls_create_option_add" = "Add an option...";
|
||||
"lng_polls_create_limit#one" = "You can add {count} more option.";
|
||||
"lng_polls_create_limit#other" = "You can add {count} more options.";
|
||||
"lng_polls_create_maximum" = "You have added the maximum number of options.";
|
||||
"lng_polls_create_button" = "Create";
|
||||
|
||||
// Wnd specific
|
||||
|
||||
|
|
|
@ -4752,36 +4752,6 @@ void ApiWrap::sendExistingDocument(
|
|||
};
|
||||
performRequest();
|
||||
|
||||
AssertIsDebug();
|
||||
auto answers = QVector<MTPPollAnswer>{
|
||||
MTP_pollAnswer(MTP_string("first option"), MTP_bytes("a")),
|
||||
MTP_pollAnswer(MTP_string("second option"), MTP_bytes("b")),
|
||||
MTP_pollAnswer(MTP_string("third very very very very very very "
|
||||
"very very very very very very option"), MTP_bytes("c")),
|
||||
MTP_pollAnswer(MTP_string("fourth option"), MTP_bytes("d")),
|
||||
};
|
||||
history->sendRequestId = request(MTPmessages_SendMedia(
|
||||
MTP_flags(sendFlags),
|
||||
peer->input,
|
||||
MTP_int(replyTo),
|
||||
MTP_inputMediaPoll(
|
||||
MTP_poll(
|
||||
MTP_long(rand_value<uint64>()),
|
||||
MTP_flags(0),
|
||||
MTP_string("Very very very very very very very very very very "
|
||||
"very very very long poll question text"),
|
||||
MTP_vector<MTPPollAnswer>(answers))),
|
||||
MTP_string(captionText),
|
||||
MTP_long(rand_value<uint64>()),
|
||||
MTPnullMarkup,
|
||||
sentEntities
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
applyUpdates(result);
|
||||
}).fail(
|
||||
base::duplicate(*failHandler)
|
||||
).afterRequest(history->sendRequestId
|
||||
).send();
|
||||
|
||||
if (const auto main = App::main()) {
|
||||
main->finishForwarding(history);
|
||||
if (document->sticker()) {
|
||||
|
@ -5344,6 +5314,45 @@ void ApiWrap::setSelfDestructDays(int days) {
|
|||
_selfDestructChanges.fire_copy(days);
|
||||
}
|
||||
|
||||
void ApiWrap::createPoll(
|
||||
const PollData &data,
|
||||
const SendOptions &options,
|
||||
FnMut<void()> done,
|
||||
FnMut<void(const RPCError &error)> fail) {
|
||||
sendAction(options);
|
||||
|
||||
const auto history = options.history;
|
||||
const auto peer = history->peer;
|
||||
auto sendFlags = MTPmessages_SendMedia::Flags(0);
|
||||
if (options.replyTo) {
|
||||
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
|
||||
}
|
||||
const auto channelPost = peer->isChannel() && !peer->isMegagroup();
|
||||
const auto silentPost = channelPost
|
||||
&& _session->data().notifySilentPosts(peer);
|
||||
if (silentPost) {
|
||||
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
|
||||
}
|
||||
|
||||
const auto replyTo = options.replyTo;
|
||||
history->sendRequestId = request(MTPmessages_SendMedia(
|
||||
MTP_flags(sendFlags),
|
||||
peer->input,
|
||||
MTP_int(replyTo),
|
||||
MTP_inputMediaPoll(PollDataToMTP(&data)),
|
||||
MTP_string(QString()),
|
||||
MTP_long(rand_value<uint64>()),
|
||||
MTPReplyMarkup(),
|
||||
MTPVector<MTPMessageEntity>()
|
||||
)).done([=, done = std::move(done)](const MTPUpdates &result) mutable {
|
||||
applyUpdates(result);
|
||||
done();
|
||||
}).fail([=, fail = std::move(fail)](const RPCError &error) mutable {
|
||||
fail(error);
|
||||
}).afterRequest(history->sendRequestId
|
||||
).send();
|
||||
}
|
||||
|
||||
void ApiWrap::sendPollVotes(
|
||||
FullMsgId itemId,
|
||||
const std::vector<QByteArray> &options) {
|
||||
|
@ -5385,29 +5394,12 @@ void ApiWrap::closePoll(FullMsgId itemId) {
|
|||
return;
|
||||
}
|
||||
|
||||
const auto convert = [](const PollAnswer &answer) {
|
||||
return MTP_pollAnswer(
|
||||
MTP_string(answer.text),
|
||||
MTP_bytes(answer.option));
|
||||
};
|
||||
|
||||
auto answers = QVector<MTPPollAnswer>();
|
||||
answers.reserve(poll->answers.size());
|
||||
ranges::transform(
|
||||
poll->answers,
|
||||
ranges::back_inserter(answers),
|
||||
convert);
|
||||
|
||||
const auto requestId = request(MTPmessages_EditMessage(
|
||||
MTP_flags(MTPmessages_EditMessage::Flag::f_media),
|
||||
item->history()->peer->input,
|
||||
MTP_int(item->id),
|
||||
MTPstring(),
|
||||
MTP_inputMediaPoll(MTP_poll(
|
||||
MTP_long(poll->id),
|
||||
MTP_flags(MTPDpoll::Flag::f_closed),
|
||||
MTP_string(poll->question),
|
||||
MTP_vector<MTPPollAnswer>(answers))),
|
||||
MTP_inputMediaPoll(PollDataToMTP(poll)),
|
||||
MTPReplyMarkup(),
|
||||
MTPVector<MTPMessageEntity>()
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
|
|
|
@ -377,6 +377,11 @@ public:
|
|||
rpl::producer<int> selfDestructValue() const;
|
||||
void saveSelfDestruct(int days);
|
||||
|
||||
void createPoll(
|
||||
const PollData &data,
|
||||
const SendOptions &options,
|
||||
FnMut<void()> done,
|
||||
FnMut<void(const RPCError &error)> fail);
|
||||
void sendPollVotes(
|
||||
FullMsgId itemId,
|
||||
const std::vector<QByteArray> &options);
|
||||
|
|
|
@ -408,7 +408,9 @@ bool Application::notify(QObject *receiver, QEvent *e) {
|
|||
_loopNestingLevel = _previousLoopNestingLevels.back();
|
||||
_previousLoopNestingLevels.pop_back();
|
||||
}
|
||||
processPostponedCalls(--_eventNestingLevel);
|
||||
const auto processTillLevel = _eventNestingLevel - 1;
|
||||
processPostponedCalls(processTillLevel);
|
||||
_eventNestingLevel = processTillLevel;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -834,3 +834,53 @@ themesScroll: ScrollArea(defaultScrollArea) {
|
|||
bottomsh: 0px;
|
||||
topsh: 0px;
|
||||
}
|
||||
|
||||
createPollField: InputField(defaultInputField) {
|
||||
font: boxTextFont;
|
||||
textMargins: margins(0px, 0px, 0px, 0px);
|
||||
textAlign: align(left);
|
||||
heightMin: 36px;
|
||||
heightMax: 86px;
|
||||
placeholderFg: placeholderFg;
|
||||
placeholderFgActive: placeholderFgActive;
|
||||
placeholderFgError: placeholderFgActive;
|
||||
placeholderMargins: margins(2px, 0px, 2px, 0px);
|
||||
placeholderAlign: align(topleft);
|
||||
placeholderScale: 0.;
|
||||
placeholderFont: boxTextFont;
|
||||
placeholderShift: -50px;
|
||||
border: 0px;
|
||||
borderActive: 0px;
|
||||
duration: 100;
|
||||
}
|
||||
createPollFieldPadding: margins(22px, 5px, 22px, 5px);
|
||||
createPollOptionField: InputField(createPollField) {
|
||||
textMargins: margins(22px, 8px, 40px, 8px);
|
||||
placeholderMargins: margins(2px, 0px, 2px, 0px);
|
||||
heightMax: 64px;
|
||||
}
|
||||
createPollLimitLabel: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 274px;
|
||||
align: align(topleft);
|
||||
}
|
||||
createPollOptionRemove: CrossButton {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
||||
cross: CrossAnimation {
|
||||
size: 22px;
|
||||
skip: 6px;
|
||||
stroke: 1px;
|
||||
minScale: 0.3;
|
||||
}
|
||||
crossFg: boxTitleCloseFg;
|
||||
crossFgOver: boxTitleCloseFgOver;
|
||||
crossPosition: point(0px, 0px);
|
||||
|
||||
duration: 150;
|
||||
loadingPeriod: 1000;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
}
|
||||
createPollOptionRemovePosition: point(10px, 7px);
|
||||
|
|
|
@ -0,0 +1,546 @@
|
|||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "boxes/create_poll_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "data/data_poll.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/widgets/input_fields.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kQuestionLimit = 255;
|
||||
constexpr auto kMaxOptionsCount = 10;
|
||||
constexpr auto kOptionLimit = 100;
|
||||
constexpr auto kWarnQuestionLimit = 80;
|
||||
constexpr auto kWarnOptionLimit = 30;
|
||||
|
||||
class Options {
|
||||
public:
|
||||
Options(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::VerticalLayout*> container);
|
||||
|
||||
[[nodiscard]] bool isValid() const;
|
||||
[[nodiscard]] rpl::producer<bool> isValidChanged() const;
|
||||
[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
|
||||
void focusFirst();
|
||||
|
||||
[[nodiscard]] rpl::producer<int> usedCount() const;
|
||||
[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
|
||||
|
||||
private:
|
||||
class Option {
|
||||
public:
|
||||
static Option Create(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
int position);
|
||||
|
||||
[[nodisacrd]] bool hasShadow() const;
|
||||
void createShadow();
|
||||
//void destroyShadow();
|
||||
|
||||
void createRemove();
|
||||
void toggleRemoveAlways(bool toggled);
|
||||
|
||||
[[nodiscard]] bool isEmpty() const;
|
||||
[[nodiscard]] bool isGood() const;
|
||||
[[nodiscard]] bool hasFocus() const;
|
||||
void setFocus() const;
|
||||
void clearValue();
|
||||
|
||||
void setPlaceholder() const;
|
||||
void removePlaceholder() const;
|
||||
|
||||
not_null<Ui::InputField*> field() const;
|
||||
|
||||
[[nodiscard]] PollAnswer toPollAnswer(char id) const;
|
||||
|
||||
[[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const;
|
||||
|
||||
private:
|
||||
Option() = default;
|
||||
|
||||
base::unique_qptr<Ui::InputField> _field;
|
||||
base::unique_qptr<Ui::PlainShadow> _shadow;
|
||||
base::unique_qptr<Ui::CrossButton> _remove;
|
||||
rpl::variable<bool> *_removeAlways = nullptr;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] bool full() const;
|
||||
//[[nodiscard]] bool correctShadows() const;
|
||||
//void fixShadows();
|
||||
void removeEmptyTail();
|
||||
void addEmptyOption();
|
||||
void checkLastOption();
|
||||
void validateState();
|
||||
void fixAfterErase();
|
||||
int findField(not_null<Ui::InputField*> field) const;
|
||||
|
||||
not_null<QWidget*> _outer;
|
||||
not_null<Ui::VerticalLayout*> _container;
|
||||
int _position = 0;
|
||||
std::vector<Option> _list;
|
||||
rpl::variable<bool> _valid = false;
|
||||
rpl::variable<int> _usedCount = 0;
|
||||
rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
|
||||
|
||||
};
|
||||
|
||||
void InitField(
|
||||
not_null<QWidget*> container,
|
||||
not_null<Ui::InputField*> field) {
|
||||
field->setInstantReplaces(Ui::InstantReplaces::Default());
|
||||
field->setInstantReplacesEnabled(Global::ReplaceEmojiValue());
|
||||
Ui::Emoji::SuggestionsController::Init(container, field);
|
||||
}
|
||||
|
||||
Options::Option Options::Option::Create(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
int position) {
|
||||
auto result = Option();
|
||||
const auto field = container->insert(
|
||||
position,
|
||||
object_ptr<Ui::InputField>(
|
||||
container,
|
||||
st::createPollOptionField,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
langFactory(lng_polls_create_option_add)));
|
||||
InitField(outer, field);
|
||||
result._field.reset(field);
|
||||
|
||||
result.createShadow();
|
||||
result.createRemove();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Options::Option::hasShadow() const {
|
||||
return (_shadow != nullptr);
|
||||
}
|
||||
|
||||
void Options::Option::createShadow() {
|
||||
Expects(_field != nullptr);
|
||||
|
||||
if (_shadow) {
|
||||
return;
|
||||
}
|
||||
const auto value = Ui::CreateChild<Ui::PlainShadow>(_field.get());
|
||||
value->show();
|
||||
_field->sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
const auto left = st::createPollFieldPadding.left();
|
||||
value->setGeometry(
|
||||
left,
|
||||
size.height() - st::lineWidth,
|
||||
size.width() - left,
|
||||
st::lineWidth);
|
||||
}, value->lifetime());
|
||||
_shadow.reset(value);
|
||||
}
|
||||
|
||||
//void Options::Option::destroyShadow() {
|
||||
// _shadow = nullptr;
|
||||
//}
|
||||
|
||||
void Options::Option::createRemove() {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
const auto field = _field.get();
|
||||
auto &lifetime = field->lifetime();
|
||||
|
||||
const auto remove = Ui::CreateChild<Ui::CrossButton>(
|
||||
field,
|
||||
st::createPollOptionRemove);
|
||||
remove->hide(anim::type::instant);
|
||||
|
||||
const auto toggle = lifetime.make_state<rpl::variable<bool>>(false);
|
||||
_removeAlways = lifetime.make_state<rpl::variable<bool>>(false);
|
||||
|
||||
QObject::connect(field, &Ui::InputField::changed, [=] {
|
||||
// Don't capture 'this'! Because Option is a value type.
|
||||
*toggle = !field->getLastText().isEmpty();
|
||||
});
|
||||
rpl::combine(
|
||||
toggle->value(),
|
||||
_removeAlways->value(),
|
||||
_1 || _2
|
||||
) | rpl::start_with_next([=](bool shown) {
|
||||
remove->toggle(shown, anim::type::normal);
|
||||
}, remove->lifetime());
|
||||
|
||||
field->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
remove->moveToRight(
|
||||
st::createPollOptionRemovePosition.x(),
|
||||
st::createPollOptionRemovePosition.y(),
|
||||
width);
|
||||
}, remove->lifetime());
|
||||
|
||||
_remove.reset(remove);
|
||||
}
|
||||
|
||||
bool Options::Option::isEmpty() const {
|
||||
return _field->getLastText().trimmed().isEmpty();
|
||||
}
|
||||
|
||||
bool Options::Option::isGood() const {
|
||||
const auto text = _field->getLastText().trimmed();
|
||||
return !text.isEmpty() && (text.size() <= kOptionLimit);
|
||||
}
|
||||
|
||||
bool Options::Option::hasFocus() const {
|
||||
return _field->hasFocus();
|
||||
}
|
||||
|
||||
void Options::Option::setFocus() const {
|
||||
_field->setFocus();
|
||||
}
|
||||
|
||||
void Options::Option::clearValue() {
|
||||
_field->setText(QString());
|
||||
}
|
||||
|
||||
void Options::Option::setPlaceholder() const {
|
||||
_field->setPlaceholder(langFactory(lng_polls_create_option_add));
|
||||
}
|
||||
|
||||
void Options::Option::toggleRemoveAlways(bool toggled) {
|
||||
*_removeAlways = toggled;
|
||||
}
|
||||
|
||||
not_null<Ui::InputField*> Options::Option::field() const {
|
||||
return _field.get();
|
||||
}
|
||||
|
||||
void Options::Option::removePlaceholder() const {
|
||||
_field->setPlaceholder(nullptr);
|
||||
}
|
||||
|
||||
PollAnswer Options::Option::toPollAnswer(char id) const {
|
||||
return PollAnswer{
|
||||
_field->getLastText().trimmed(),
|
||||
QByteArray(1, id)
|
||||
};
|
||||
}
|
||||
|
||||
rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
|
||||
return _remove->clicks();
|
||||
}
|
||||
|
||||
Options::Options(
|
||||
not_null<QWidget*> outer,
|
||||
not_null<Ui::VerticalLayout*> container)
|
||||
: _outer(outer)
|
||||
, _container(container)
|
||||
, _position(_container->count()) {
|
||||
checkLastOption();
|
||||
}
|
||||
|
||||
bool Options::full() const {
|
||||
return (_list.size() == kMaxOptionsCount);
|
||||
}
|
||||
|
||||
bool Options::isValid() const {
|
||||
return _valid.current();
|
||||
}
|
||||
|
||||
rpl::producer<bool> Options::isValidChanged() const {
|
||||
return _valid.changes();
|
||||
}
|
||||
|
||||
rpl::producer<int> Options::usedCount() const {
|
||||
return _usedCount.value();
|
||||
}
|
||||
|
||||
rpl::producer<not_null<QWidget*>> Options::scrollToWidget() const {
|
||||
return _scrollToWidget.events();
|
||||
}
|
||||
|
||||
std::vector<PollAnswer> Options::toPollAnswers() const {
|
||||
auto result = std::vector<PollAnswer>();
|
||||
result.reserve(_list.size());
|
||||
auto counter = char(0);
|
||||
const auto makeAnswer = [&](const Option &option) {
|
||||
return option.toPollAnswer(++counter);
|
||||
};
|
||||
ranges::copy(
|
||||
_list
|
||||
| ranges::view::filter(&Option::isGood)
|
||||
| ranges::view::transform(makeAnswer),
|
||||
ranges::back_inserter(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
void Options::focusFirst() {
|
||||
Expects(!_list.empty());
|
||||
|
||||
_list.front().setFocus();
|
||||
}
|
||||
//
|
||||
//bool Options::correctShadows() const {
|
||||
// // Last one should be without shadow if all options were used.
|
||||
// const auto noShadow = ranges::find(
|
||||
// _list,
|
||||
// true,
|
||||
// ranges::not_fn(&Option::hasShadow));
|
||||
// return (noShadow == end(_list) - (full() ? 1 : 0));
|
||||
//}
|
||||
//
|
||||
//void Options::fixShadows() {
|
||||
// if (correctShadows()) {
|
||||
// return;
|
||||
// }
|
||||
// for (auto &option : _list) {
|
||||
// option.createShadow();
|
||||
// }
|
||||
// if (full()) {
|
||||
// _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();
|
||||
auto reversed = ranges::view::reverse(_list);
|
||||
const auto emptyItem = ranges::find_if(
|
||||
reversed,
|
||||
ranges::not_fn(&Option::isEmpty)).base();
|
||||
const auto focusLast = (focused > emptyItem) && (focused < end);
|
||||
if (emptyItem == end) {
|
||||
return;
|
||||
}
|
||||
if (focusLast) {
|
||||
emptyItem->setFocus();
|
||||
}
|
||||
_list.erase(emptyItem + 1, end);
|
||||
fixAfterErase();
|
||||
}
|
||||
|
||||
void Options::fixAfterErase() {
|
||||
Expects(!_list.empty());
|
||||
|
||||
const auto last = _list.end() - 1;
|
||||
last->setPlaceholder();
|
||||
last->toggleRemoveAlways(false);
|
||||
if (last != begin(_list)) {
|
||||
(last - 1)->setPlaceholder();
|
||||
(last - 1)->toggleRemoveAlways(false);
|
||||
}
|
||||
}
|
||||
|
||||
void Options::addEmptyOption() {
|
||||
if (full()) {
|
||||
return;
|
||||
} else if (!_list.empty() && _list.back().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (_list.size() > 1) {
|
||||
(_list.end() - 2)->removePlaceholder();
|
||||
(_list.end() - 2)->toggleRemoveAlways(true);
|
||||
}
|
||||
_list.push_back(Option::Create(
|
||||
_outer,
|
||||
_container,
|
||||
_position + _list.size()));
|
||||
const auto field = _list.back().field();
|
||||
QObject::connect(field, &Ui::InputField::submitted, [=] {
|
||||
const auto index = findField(field);
|
||||
if (_list[index].isGood() && index + 1 < _list.size()) {
|
||||
_list[index + 1].setFocus();
|
||||
}
|
||||
});
|
||||
QObject::connect(field, &Ui::InputField::changed, [=] {
|
||||
Ui::PostponeCall(crl::guard(field, [=] {
|
||||
validateState();
|
||||
}));
|
||||
});
|
||||
QObject::connect(field, &Ui::InputField::focused, [=] {
|
||||
_scrollToWidget.fire_copy(field);
|
||||
});
|
||||
|
||||
_list.back().removeClicks(
|
||||
) | rpl::start_with_next([=] {
|
||||
Ui::PostponeCall(crl::guard(field, [=] {
|
||||
Expects(!_list.empty());
|
||||
|
||||
const auto item = begin(_list) + findField(field);
|
||||
if (item == _list.end() - 1) {
|
||||
item->clearValue();
|
||||
return;
|
||||
}
|
||||
if (item->hasFocus()) {
|
||||
(item + 1)->setFocus();
|
||||
}
|
||||
_list.erase(item);
|
||||
fixAfterErase();
|
||||
validateState();
|
||||
}));
|
||||
}, field->lifetime());
|
||||
|
||||
//fixShadows();
|
||||
}
|
||||
|
||||
void Options::validateState() {
|
||||
checkLastOption();
|
||||
_valid = (ranges::count_if(_list, &Option::isGood) > 1);
|
||||
const auto lastEmpty = !_list.empty() && _list.back().isEmpty();
|
||||
_usedCount = _list.size() - (lastEmpty ? 1 : 0);
|
||||
}
|
||||
|
||||
int Options::findField(not_null<Ui::InputField*> field) const {
|
||||
const auto result = ranges::find(
|
||||
_list,
|
||||
field,
|
||||
&Option::field) - begin(_list);
|
||||
|
||||
Ensures(result >= 0 && result < _list.size());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void Options::checkLastOption() {
|
||||
removeEmptyTail();
|
||||
addEmptyOption();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CreatePollBox::CreatePollBox(QWidget*) {
|
||||
}
|
||||
|
||||
rpl::producer<PollData> CreatePollBox::submitRequests() const {
|
||||
return _submitRequests.events();
|
||||
}
|
||||
|
||||
void CreatePollBox::setInnerFocus() {
|
||||
_setInnerFocus();
|
||||
}
|
||||
|
||||
void CreatePollBox::submitFailed(const QString &error) {
|
||||
Ui::Toast::Show(error);
|
||||
}
|
||||
|
||||
not_null<Ui::InputField*> CreatePollBox::setupQuestion(
|
||||
not_null<Ui::VerticalLayout*> container) {
|
||||
using namespace Settings;
|
||||
|
||||
AddSubsectionTitle(container, lng_polls_create_question);
|
||||
const auto question = container->add(
|
||||
object_ptr<Ui::InputField>(
|
||||
container,
|
||||
st::createPollField,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
langFactory(lng_polls_create_question_placeholder)),
|
||||
st::createPollFieldPadding);
|
||||
InitField(getDelegate()->outerContainer(), question);
|
||||
|
||||
return question;
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
using namespace Settings;
|
||||
|
||||
const auto id = rand_value<uint64>();
|
||||
const auto valid = lifetime().make_state<rpl::event_stream<bool>>();
|
||||
|
||||
auto result = object_ptr<Ui::VerticalLayout>(this);
|
||||
const auto container = result.data();
|
||||
|
||||
const auto question = setupQuestion(container);
|
||||
AddDivider(container);
|
||||
AddSkip(container);
|
||||
AddSubsectionTitle(container, lng_polls_create_options);
|
||||
const auto options = lifetime().make_state<Options>(
|
||||
getDelegate()->outerContainer(),
|
||||
container);
|
||||
auto limit = options->usedCount() | rpl::map([=](int count) {
|
||||
return (count < kMaxOptionsCount)
|
||||
? lng_polls_create_limit(lt_count, kMaxOptionsCount - count)
|
||||
: lang(lng_polls_create_maximum);
|
||||
}) | rpl::after_next([=] {
|
||||
container->resizeToWidth(container->widthNoMargins());
|
||||
});
|
||||
container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
std::move(limit),
|
||||
st::createPollLimitLabel),
|
||||
st::createPollFieldPadding);
|
||||
|
||||
const auto isValidQuestion = [=] {
|
||||
const auto text = question->getLastText().trimmed();
|
||||
return !text.isEmpty() && (text.size() <= kQuestionLimit);
|
||||
};
|
||||
|
||||
connect(question, &Ui::InputField::submitted, [=] {
|
||||
if (isValidQuestion()) {
|
||||
options->focusFirst();
|
||||
}
|
||||
});
|
||||
|
||||
_setInnerFocus = [=] {
|
||||
question->setFocusFast();
|
||||
};
|
||||
|
||||
const auto collectResult = [=] {
|
||||
auto result = PollData(id);
|
||||
result.question = question->getLastText().trimmed();
|
||||
result.answers = options->toPollAnswers();
|
||||
return result;
|
||||
};
|
||||
const auto updateValid = [=] {
|
||||
valid->fire(isValidQuestion() && options->isValid());
|
||||
};
|
||||
valid->events(
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::start_with_next([=](bool valid) {
|
||||
clearButtons();
|
||||
if (valid) {
|
||||
addButton(
|
||||
langFactory(lng_polls_create_button),
|
||||
[=] { _submitRequests.fire(collectResult()); });
|
||||
}
|
||||
addButton(langFactory(lng_cancel), [=] { closeBox(); });
|
||||
}, lifetime());
|
||||
|
||||
options->isValidChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateValid();
|
||||
}, lifetime());
|
||||
|
||||
options->scrollToWidget(
|
||||
) | rpl::start_with_next([=](not_null<QWidget*> widget) {
|
||||
scrollToWidget(widget);
|
||||
}, lifetime());
|
||||
|
||||
return std::move(result);
|
||||
}
|
||||
|
||||
void CreatePollBox::prepare() {
|
||||
setTitle(langFactory(lng_polls_create_title));
|
||||
|
||||
const auto inner = setInnerWidget(setupContent());
|
||||
|
||||
setDimensionsToContent(st::boxWideWidth, inner);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "boxes/abstract_box.h"
|
||||
|
||||
namespace Ui {
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
struct PollData;
|
||||
|
||||
class CreatePollBox : public BoxContent {
|
||||
public:
|
||||
CreatePollBox(QWidget*);
|
||||
|
||||
rpl::producer<PollData> submitRequests() const;
|
||||
void submitFailed(const QString &error);
|
||||
|
||||
void setInnerFocus() override;
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
|
||||
private:
|
||||
object_ptr<Ui::RpWidget> setupContent();
|
||||
not_null<Ui::InputField*> setupQuestion(
|
||||
not_null<Ui::VerticalLayout*> container);
|
||||
|
||||
Fn<void()> _setInnerFocus;
|
||||
Fn<rpl::producer<bool>()> _dataIsValidValue;
|
||||
rpl::event_stream<PollData> _submitRequests;
|
||||
|
||||
};
|
|
@ -125,3 +125,22 @@ bool PollData::applyResultToAnswers(
|
|||
bool PollData::voted() const {
|
||||
return ranges::find(answers, true, &PollAnswer::chosen) != end(answers);
|
||||
}
|
||||
|
||||
MTPPoll PollDataToMTP(not_null<const PollData*> poll) {
|
||||
const auto convert = [](const PollAnswer &answer) {
|
||||
return MTP_pollAnswer(
|
||||
MTP_string(answer.text),
|
||||
MTP_bytes(answer.option));
|
||||
};
|
||||
auto answers = QVector<MTPPollAnswer>();
|
||||
answers.reserve(poll->answers.size());
|
||||
ranges::transform(
|
||||
poll->answers,
|
||||
ranges::back_inserter(answers),
|
||||
convert);
|
||||
return MTP_poll(
|
||||
MTP_long(poll->id),
|
||||
MTP_flags(MTPDpoll::Flag::f_closed),
|
||||
MTP_string(poll->question),
|
||||
MTP_vector<MTPPollAnswer>(answers));
|
||||
}
|
||||
|
|
|
@ -48,3 +48,5 @@ private:
|
|||
bool isMinResults);
|
||||
|
||||
};
|
||||
|
||||
MTPPoll PollDataToMTP(not_null<const PollData*> poll);
|
||||
|
|
|
@ -234,8 +234,8 @@ void ForceFullRepaint(not_null<QWidget*> widget) {
|
|||
}
|
||||
|
||||
void PostponeCall(FnMut<void()> &&callable) {
|
||||
const auto app = static_cast<Application*>(qApp);
|
||||
app->postponeCall(std::move(callable));
|
||||
const auto application = static_cast<Application*>(qApp);
|
||||
application->postponeCall(std::move(callable));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
|
|
|
@ -2160,7 +2160,11 @@ void InputField::handleContentsChanged() {
|
|||
|
||||
if (tagsChanged || (_lastTextWithTags.text != currentText)) {
|
||||
_lastTextWithTags.text = currentText;
|
||||
const auto weak = make_weak(this);
|
||||
emit changed();
|
||||
if (!weak) {
|
||||
return;
|
||||
}
|
||||
checkContentHeight();
|
||||
}
|
||||
startPlaceholderAnimation();
|
||||
|
|
|
@ -15,6 +15,10 @@ class VerticalLayout : public RpWidget {
|
|||
public:
|
||||
using RpWidget::RpWidget;
|
||||
|
||||
int count() const {
|
||||
return _rows.size();
|
||||
}
|
||||
|
||||
template <
|
||||
typename Widget,
|
||||
typename = std::enable_if_t<
|
||||
|
@ -36,7 +40,7 @@ public:
|
|||
Widget *add(
|
||||
object_ptr<Widget> &&child,
|
||||
const style::margins &margin = style::margins()) {
|
||||
return insert(_rows.size(), std::move(child), margin);
|
||||
return insert(count(), std::move(child), margin);
|
||||
}
|
||||
|
||||
QMargins getMargins() const override;
|
||||
|
|
|
@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "boxes/mute_settings_box.h"
|
||||
#include "boxes/add_contact_box.h"
|
||||
#include "boxes/report_box.h"
|
||||
#include "boxes/create_poll_box.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "boxes/peers/manage_peer_box.h"
|
||||
#include "boxes/peers/edit_peer_info_box.h"
|
||||
|
@ -360,6 +361,11 @@ void Filler::addChatActions(not_null<ChatData*> chat) {
|
|||
lang(lng_profile_add_participant),
|
||||
[chat] { AddChatMembers(chat); });
|
||||
}
|
||||
if (chat->canWrite()) {
|
||||
_addAction(
|
||||
lang(lng_polls_create),
|
||||
[=] { PeerMenuCreatePoll(chat); });
|
||||
}
|
||||
_addAction(
|
||||
lang(lng_profile_export_chat),
|
||||
[=] { PeerMenuExportChat(chat); });
|
||||
|
@ -397,6 +403,11 @@ void Filler::addChannelActions(not_null<ChannelData*> channel) {
|
|||
lang(lng_channel_add_members),
|
||||
[channel] { PeerMenuAddChannelMembers(channel); });
|
||||
}
|
||||
if (channel->canWrite()) {
|
||||
_addAction(
|
||||
lang(lng_polls_create),
|
||||
[=] { PeerMenuCreatePoll(channel); });
|
||||
}
|
||||
_addAction(
|
||||
lang(isGroup
|
||||
? lng_profile_export_chat
|
||||
|
@ -615,6 +626,19 @@ void PeerMenuShareContactBox(not_null<UserData*> user) {
|
|||
}));
|
||||
}
|
||||
|
||||
void PeerMenuCreatePoll(not_null<PeerData*> peer) {
|
||||
const auto box = Ui::show(Box<CreatePollBox>());
|
||||
box->submitRequests(
|
||||
) | rpl::start_with_next([=](const PollData &result) {
|
||||
const auto options = ApiWrap::SendOptions(App::history(peer));
|
||||
Auth().api().createPoll(result, options, crl::guard(box, [=] {
|
||||
box->closeBox();
|
||||
}), crl::guard(box, [=](const RPCError &error) {
|
||||
box->submitFailed(lang(lng_attach_failed));
|
||||
}));
|
||||
}, box->lifetime());
|
||||
}
|
||||
|
||||
QPointer<Ui::RpWidget> ShowForwardMessagesBox(
|
||||
MessageIdsList &&items,
|
||||
FnMut<void()> &&successCallback) {
|
||||
|
|
|
@ -50,6 +50,7 @@ void PeerMenuShareContactBox(not_null<UserData*> user);
|
|||
void PeerMenuAddContact(not_null<UserData*> user);
|
||||
void PeerMenuAddChannelMembers(not_null<ChannelData*> channel);
|
||||
//void PeerMenuUngroupFeed(not_null<Data::Feed*> feed); // #feed
|
||||
void PeerMenuCreatePoll(not_null<PeerData*> peer);
|
||||
|
||||
//void ToggleChannelGrouping(not_null<ChannelData*> channel, bool group); // #feed
|
||||
Fn<void()> ClearHistoryHandler(not_null<PeerData*> peer);
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
<(src_loc)/boxes/confirm_phone_box.h
|
||||
<(src_loc)/boxes/connection_box.cpp
|
||||
<(src_loc)/boxes/connection_box.h
|
||||
<(src_loc)/boxes/create_poll_box.cpp
|
||||
<(src_loc)/boxes/create_poll_box.h
|
||||
<(src_loc)/boxes/download_path_box.cpp
|
||||
<(src_loc)/boxes/download_path_box.h
|
||||
<(src_loc)/boxes/edit_caption_box.cpp
|
||||
|
|
Loading…
Reference in New Issue