mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-07-25 06:54:43 +00:00
[poll-create] Added files support for poll media.
This commit is contained in:
@@ -6875,6 +6875,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_polls_solution_title" = "Explanation";
|
||||
"lng_polls_solution_placeholder" = "Add a Comment (Optional)";
|
||||
"lng_polls_solution_about" = "Users will see this comment after choosing a wrong answer, good for educational purposes.";
|
||||
"lng_polls_media_uploading" = "Please wait until media upload is complete.";
|
||||
|
||||
"lng_polls_poll_results_title" = "Poll results";
|
||||
"lng_polls_quiz_results_title" = "Quiz results";
|
||||
|
||||
@@ -15,9 +15,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#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/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "data/data_document.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/history_view_schedule_box.h"
|
||||
@@ -27,8 +32,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#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_media_prepare.h"
|
||||
#include "ui/controls/emoji_button.h"
|
||||
#include "ui/controls/emoji_button_factory.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/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/vertical_list.h"
|
||||
@@ -36,15 +51,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.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/ui_utility.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "apiwrap.h"
|
||||
#include "styles/style_boxes.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_polls.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
@@ -59,18 +78,281 @@ constexpr auto kSolutionLimit = 200;
|
||||
constexpr auto kWarnSolutionLimit = 60;
|
||||
constexpr auto kErrorLimit = 99;
|
||||
|
||||
struct PollMediaState {
|
||||
std::optional<MTPInputMedia> media;
|
||||
std::shared_ptr<Ui::DynamicImage> thumbnail;
|
||||
bool rounded = false;
|
||||
bool uploading = false;
|
||||
float64 progress = 0.;
|
||||
uint64 uploadDataId = 0;
|
||||
uint64 token = 0;
|
||||
Fn<void()> update;
|
||||
};
|
||||
|
||||
class LocalImageThumbnail final : public Ui::DynamicImage {
|
||||
public:
|
||||
explicit LocalImageThumbnail(QImage original)
|
||||
: _original(std::move(original)) {
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> clone() override {
|
||||
return std::make_shared<LocalImageThumbnail>(_original);
|
||||
}
|
||||
|
||||
QImage image(int size) override {
|
||||
return _original;
|
||||
}
|
||||
|
||||
void subscribeToUpdates(Fn<void()> callback) override {
|
||||
}
|
||||
|
||||
private:
|
||||
QImage _original;
|
||||
|
||||
};
|
||||
|
||||
class PollMediaButton final : public Ui::RippleButton {
|
||||
public:
|
||||
PollMediaButton(
|
||||
not_null<QWidget*> parent,
|
||||
const style::IconButton &st,
|
||||
std::shared_ptr<PollMediaState> state)
|
||||
: Ui::RippleButton(parent, st.ripple)
|
||||
, _st(st)
|
||||
, _state(std::move(state))
|
||||
, _attach(Ui::MakeIconThumbnail(_st.icon))
|
||||
, _attachOver(_st.iconOver.empty()
|
||||
? _attach
|
||||
: Ui::MakeIconThumbnail(_st.iconOver))
|
||||
, _radial([=](crl::time now) { radialAnimationCallback(now); }) {
|
||||
const auto weak = QPointer<PollMediaButton>(this);
|
||||
_state->update = [=] {
|
||||
if (weak) {
|
||||
weak->updateMediaSubscription();
|
||||
weak->update();
|
||||
}
|
||||
};
|
||||
resize(_st.width, _st.height);
|
||||
setPointerCursor(true);
|
||||
updateMediaSubscription();
|
||||
}
|
||||
|
||||
~PollMediaButton() override {
|
||||
if (_subscribed) {
|
||||
_subscribed->subscribeToUpdates(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override {
|
||||
auto p = Painter(this);
|
||||
paintRipple(p, _st.rippleAreaPosition);
|
||||
if (const auto image = _state->thumbnail
|
||||
? _state->thumbnail
|
||||
: currentAttachThumbnail()) {
|
||||
const auto target = _state->thumbnail
|
||||
? rippleRect()
|
||||
: iconRect();
|
||||
if (_state->thumbnail) {
|
||||
paintCover(
|
||||
p,
|
||||
target,
|
||||
image->image(std::max(target.width(), target.height())),
|
||||
_state->rounded);
|
||||
} else {
|
||||
p.drawImage(
|
||||
target,
|
||||
image->image(std::max(target.width(), target.height())));
|
||||
}
|
||||
}
|
||||
if (_state->uploading && !_radial.animating()) {
|
||||
_radial.start(_state->progress);
|
||||
}
|
||||
if (_state->uploading || _radial.animating()) {
|
||||
if (_state->thumbnail) {
|
||||
p.save();
|
||||
auto path = QPainterPath();
|
||||
path.addRoundedRect(
|
||||
rippleRect(),
|
||||
st::roundRadiusSmall,
|
||||
st::roundRadiusSmall);
|
||||
p.setClipPath(path);
|
||||
p.fillRect(rippleRect(), st::songCoverOverlayFg);
|
||||
p.restore();
|
||||
}
|
||||
const auto line = float64(st::lineWidth * 2);
|
||||
const auto margin = float64(st::pollAttachProgressMargin);
|
||||
const auto arc = QRectF(rippleRect()) - Margins(margin);
|
||||
_radial.draw(p, arc, line, st::historyFileThumbRadialFg);
|
||||
}
|
||||
}
|
||||
|
||||
void onStateChanged(State was, StateChangeSource source) override {
|
||||
RippleButton::onStateChanged(was, source);
|
||||
if (!_state->thumbnail) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
QImage prepareRippleMask() const override {
|
||||
return Ui::RippleAnimation::EllipseMask(QSize(
|
||||
_st.rippleAreaSize,
|
||||
_st.rippleAreaSize));
|
||||
}
|
||||
|
||||
QPoint prepareRippleStartPosition() const override {
|
||||
auto result = mapFromGlobal(QCursor::pos())
|
||||
- _st.rippleAreaPosition;
|
||||
const auto rect = QRect(
|
||||
QPoint(),
|
||||
QSize(_st.rippleAreaSize, _st.rippleAreaSize));
|
||||
return rect.contains(result)
|
||||
? result
|
||||
: DisabledRippleStartPosition();
|
||||
}
|
||||
|
||||
private:
|
||||
void paintCover(
|
||||
Painter &p,
|
||||
QRect target,
|
||||
QImage image,
|
||||
bool rounded) const {
|
||||
if (image.isNull() || target.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto source = QRectF(
|
||||
0,
|
||||
0,
|
||||
image.width(),
|
||||
image.height());
|
||||
const auto kx = target.width() / source.width();
|
||||
const auto ky = target.height() / source.height();
|
||||
const auto scale = std::max(kx, ky);
|
||||
const auto size = QSizeF(
|
||||
source.width() * scale,
|
||||
source.height() * scale);
|
||||
const auto geometry = QRectF(
|
||||
target.x() + (target.width() - size.width()) / 2.,
|
||||
target.y() + (target.height() - size.height()) / 2.,
|
||||
size.width(),
|
||||
size.height());
|
||||
p.save();
|
||||
if (rounded) {
|
||||
auto path = QPainterPath();
|
||||
path.addRoundedRect(
|
||||
target,
|
||||
st::roundRadiusSmall,
|
||||
st::roundRadiusSmall);
|
||||
p.setClipPath(path);
|
||||
}
|
||||
p.drawImage(geometry, image, source);
|
||||
p.restore();
|
||||
}
|
||||
|
||||
void radialAnimationCallback(crl::time now) {
|
||||
const auto updated = _radial.update(
|
||||
_state->progress,
|
||||
!_state->uploading,
|
||||
now);
|
||||
if (!anim::Disabled() || updated || _radial.animating()) {
|
||||
update(rippleRect());
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] QRect rippleRect() const {
|
||||
return QRect(
|
||||
_st.rippleAreaPosition,
|
||||
QSize(_st.rippleAreaSize, _st.rippleAreaSize));
|
||||
}
|
||||
|
||||
[[nodiscard]] QRect iconRect() const {
|
||||
const auto over = isOver() || isDown();
|
||||
const auto &icon = over && !_st.iconOver.empty()
|
||||
? _st.iconOver
|
||||
: _st.icon;
|
||||
auto position = _st.iconPosition;
|
||||
if (position.x() < 0) {
|
||||
position.setX((width() - icon.width()) / 2);
|
||||
}
|
||||
if (position.y() < 0) {
|
||||
position.setY((height() - icon.height()) / 2);
|
||||
}
|
||||
return QRect(position, QSize(icon.width(), icon.height()));
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> currentAttachThumbnail() const {
|
||||
return (isOver() || isDown()) ? _attachOver : _attach;
|
||||
}
|
||||
|
||||
void updateMediaSubscription() {
|
||||
if (_subscribed == _state->thumbnail) {
|
||||
return;
|
||||
}
|
||||
if (_subscribed) {
|
||||
_subscribed->subscribeToUpdates(nullptr);
|
||||
}
|
||||
_subscribed = _state->thumbnail;
|
||||
if (!_subscribed) {
|
||||
return;
|
||||
}
|
||||
const auto weak = QPointer<PollMediaButton>(this);
|
||||
_subscribed->subscribeToUpdates([=] {
|
||||
if (weak) {
|
||||
weak->update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const style::IconButton &_st;
|
||||
const std::shared_ptr<PollMediaState> _state;
|
||||
const std::shared_ptr<Ui::DynamicImage> _attach;
|
||||
const std::shared_ptr<Ui::DynamicImage> _attachOver;
|
||||
std::shared_ptr<Ui::DynamicImage> _subscribed;
|
||||
Ui::RadialAnimation _radial;
|
||||
|
||||
};
|
||||
|
||||
class PreparePollMediaTask final : public Task {
|
||||
public:
|
||||
PreparePollMediaTask(
|
||||
FileLoadTask::Args &&args,
|
||||
Fn<void(std::shared_ptr<FilePrepareResult>)> done)
|
||||
: _task(std::move(args))
|
||||
, _done(std::move(done)) {
|
||||
}
|
||||
|
||||
void process() override {
|
||||
_task.process({ .generateGoodThumbnail = false });
|
||||
}
|
||||
|
||||
void finish() override {
|
||||
_done(_task.peekResult());
|
||||
}
|
||||
|
||||
private:
|
||||
FileLoadTask _task;
|
||||
Fn<void(std::shared_ptr<FilePrepareResult>)> _done;
|
||||
|
||||
};
|
||||
|
||||
class Options {
|
||||
public:
|
||||
using AttachCallback = Fn<void(
|
||||
not_null<Ui::RpWidget*>,
|
||||
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);
|
||||
bool chooseCorrectEnabled,
|
||||
AttachCallback attachCallback);
|
||||
|
||||
[[nodiscard]] bool hasOptions() const;
|
||||
[[nodiscard]] bool isValid() const;
|
||||
[[nodiscard]] bool hasCorrect() const;
|
||||
[[nodiscard]] bool hasUploadingMedia() const;
|
||||
[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
|
||||
void focusFirst();
|
||||
|
||||
@@ -89,7 +371,8 @@ private:
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session,
|
||||
int position,
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> group);
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> group,
|
||||
AttachCallback attachCallback);
|
||||
|
||||
Option(const Option &other) = delete;
|
||||
Option &operator=(const Option &other) = delete;
|
||||
@@ -109,6 +392,7 @@ private:
|
||||
[[nodiscard]] bool isGood() const;
|
||||
[[nodiscard]] bool isTooLong() const;
|
||||
[[nodiscard]] bool isCorrect() const;
|
||||
[[nodiscard]] bool uploadingMedia() const;
|
||||
[[nodiscard]] bool hasFocus() const;
|
||||
void setFocus() const;
|
||||
void clearValue();
|
||||
@@ -124,6 +408,7 @@ private:
|
||||
|
||||
private:
|
||||
void createRemove();
|
||||
void createAttach();
|
||||
void createWarning();
|
||||
void toggleCorrectSpace(bool visible);
|
||||
void updateFieldGeometry();
|
||||
@@ -135,8 +420,11 @@ private:
|
||||
bool _hasCorrect = false;
|
||||
Ui::InputField *_field = nullptr;
|
||||
base::unique_qptr<Ui::PlainShadow> _shadow;
|
||||
base::unique_qptr<PollMediaButton> _attach;
|
||||
base::unique_qptr<Ui::CrossButton> _remove;
|
||||
rpl::variable<bool> *_removeAlways = nullptr;
|
||||
AttachCallback _attachCallback;
|
||||
std::shared_ptr<PollMediaState> _media;
|
||||
|
||||
};
|
||||
|
||||
@@ -158,6 +446,7 @@ private:
|
||||
not_null<Ui::VerticalLayout*> _container;
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
ChatHelpers::TabbedPanel * const _emojiPanel;
|
||||
const AttachCallback _attachCallback;
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> _chooseCorrectGroup;
|
||||
int _position = 0;
|
||||
std::vector<std::unique_ptr<Option>> _list;
|
||||
@@ -247,7 +536,8 @@ Options::Option::Option(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session,
|
||||
int position,
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> group)
|
||||
std::shared_ptr<Ui::RadiobuttonGroup> group,
|
||||
AttachCallback attachCallback)
|
||||
: _wrap(container->insert(
|
||||
position,
|
||||
object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
|
||||
@@ -261,7 +551,9 @@ Options::Option::Option(
|
||||
? st::createPollOptionFieldPremium
|
||||
: st::createPollOptionField,
|
||||
Ui::InputField::Mode::NoNewlines,
|
||||
tr::lng_polls_create_option_add())) {
|
||||
tr::lng_polls_create_option_add()))
|
||||
, _attachCallback(std::move(attachCallback))
|
||||
, _media(std::make_shared<PollMediaState>()) {
|
||||
InitField(outer, _field, session);
|
||||
_field->setMaxLength(kOptionLimit + kErrorLimit);
|
||||
_field->show();
|
||||
@@ -289,6 +581,7 @@ Options::Option::Option(
|
||||
|
||||
createShadow();
|
||||
createRemove();
|
||||
createAttach();
|
||||
createWarning();
|
||||
enableChooseCorrect(group);
|
||||
_correctShown.stop();
|
||||
@@ -356,8 +649,9 @@ void Options::Option::createRemove() {
|
||||
|
||||
field->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto attachSkip = st::pollAttach.width + st::lineWidth * 4;
|
||||
remove->moveToRight(
|
||||
st::createPollOptionRemovePosition.x(),
|
||||
st::createPollOptionRemovePosition.x() + attachSkip,
|
||||
st::createPollOptionRemovePosition.y(),
|
||||
width);
|
||||
}, remove->lifetime());
|
||||
@@ -365,6 +659,32 @@ void Options::Option::createRemove() {
|
||||
_remove.reset(remove);
|
||||
}
|
||||
|
||||
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());
|
||||
_attach.reset(attach);
|
||||
}
|
||||
|
||||
void Options::Option::createWarning() {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
@@ -405,6 +725,10 @@ bool Options::Option::isCorrect() const {
|
||||
return isGood() && _correct && _correct->entity()->Checkbox::checked();
|
||||
}
|
||||
|
||||
bool Options::Option::uploadingMedia() const {
|
||||
return _media->uploading;
|
||||
}
|
||||
|
||||
bool Options::Option::hasFocus() const {
|
||||
return field()->hasFocus();
|
||||
}
|
||||
@@ -502,6 +826,7 @@ PollAnswer Options::Option::toPollAnswer(int index) const {
|
||||
},
|
||||
QByteArray(1, ('0' + index)),
|
||||
};
|
||||
result.media = _media->media;
|
||||
TextUtilities::Trim(result.text);
|
||||
result.correct = _correct ? _correct->entity()->Checkbox::checked() : false;
|
||||
return result;
|
||||
@@ -516,11 +841,13 @@ Options::Options(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Window::SessionController*> controller,
|
||||
ChatHelpers::TabbedPanel *emojiPanel,
|
||||
bool chooseCorrectEnabled)
|
||||
bool chooseCorrectEnabled,
|
||||
AttachCallback attachCallback)
|
||||
: _box(box)
|
||||
, _container(container)
|
||||
, _controller(controller)
|
||||
, _emojiPanel(emojiPanel)
|
||||
, _attachCallback(std::move(attachCallback))
|
||||
, _chooseCorrectGroup(chooseCorrectEnabled
|
||||
? createChooseCorrectGroup()
|
||||
: nullptr)
|
||||
@@ -545,6 +872,10 @@ bool Options::hasCorrect() const {
|
||||
return _hasCorrect;
|
||||
}
|
||||
|
||||
bool Options::hasUploadingMedia() const {
|
||||
return ranges::any_of(_list, &Option::uploadingMedia);
|
||||
}
|
||||
|
||||
rpl::producer<int> Options::usedCount() const {
|
||||
return _usedCount.value();
|
||||
}
|
||||
@@ -695,7 +1026,8 @@ void Options::addEmptyOption() {
|
||||
_container,
|
||||
&_controller->session(),
|
||||
_position + _list.size() + _destroyed.size(),
|
||||
_chooseCorrectGroup));
|
||||
_chooseCorrectGroup,
|
||||
_attachCallback));
|
||||
const auto field = _list.back()->field();
|
||||
if (const auto emojiPanel = _emojiPanel) {
|
||||
const auto emojiToggle = Ui::AddEmojiToggleToField(
|
||||
@@ -836,12 +1168,14 @@ void Options::checkLastOption() {
|
||||
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)
|
||||
@@ -980,7 +1314,7 @@ not_null<Ui::InputField*> CreatePollBox::setupSolution(
|
||||
const auto solution = inner->add(
|
||||
object_ptr<Ui::InputField>(
|
||||
inner,
|
||||
st::createPollSolutionField,
|
||||
st::pollMediaField,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::lng_polls_solution_placeholder()),
|
||||
st::createPollFieldPadding);
|
||||
@@ -1039,19 +1373,310 @@ 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;
|
||||
};
|
||||
struct State final {
|
||||
Errors error = Error::Question;
|
||||
std::unique_ptr<Options> options;
|
||||
rpl::event_stream<bool> multipleForceOff;
|
||||
rpl::event_stream<bool> quizForceOff;
|
||||
std::shared_ptr<PollMediaState> descriptionMedia
|
||||
= std::make_shared<PollMediaState>();
|
||||
std::shared_ptr<PollMediaState> solutionMedia
|
||||
= std::make_shared<PollMediaState>();
|
||||
base::flat_map<FullMsgId, UploadContext> uploads;
|
||||
base::unique_qptr<Ui::PopupMenu> mediaMenu;
|
||||
std::unique_ptr<TaskQueue> prepareQueue;
|
||||
};
|
||||
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,
|
||||
std::optional<MTPInputMedia> value,
|
||||
std::shared_ptr<Ui::DynamicImage> thumbnail,
|
||||
bool rounded) {
|
||||
media->token++;
|
||||
media->media = std::move(value);
|
||||
media->thumbnail = std::move(thumbnail);
|
||||
media->rounded = rounded;
|
||||
media->progress = (media->uploading && media->media.has_value())
|
||||
? 1.
|
||||
: 0.;
|
||||
media->uploadDataId = 0;
|
||||
media->uploading = false;
|
||||
updateMedia(media);
|
||||
};
|
||||
struct UploadedMedia final {
|
||||
std::optional<MTPInputMedia> input;
|
||||
std::shared_ptr<Ui::DynamicImage> thumbnail;
|
||||
};
|
||||
const auto parseUploaded = [=](
|
||||
const MTPMessageMedia &result,
|
||||
FullMsgId fullId) {
|
||||
auto parsed = UploadedMedia{
|
||||
.input = PollMediaToInputMedia(result),
|
||||
};
|
||||
result.match([&](const MTPDmessageMediaPhoto &media) {
|
||||
if (const auto photo = media.vphoto()) {
|
||||
photo->match([&](const MTPDphoto &) {
|
||||
parsed.thumbnail = Ui::MakePhotoThumbnail(
|
||||
_controller->session().data().processPhoto(
|
||||
*photo),
|
||||
fullId);
|
||||
}, [](const auto &) {
|
||||
});
|
||||
}
|
||||
}, [&](const MTPDmessageMediaDocument &media) {
|
||||
if (const auto document = media.vdocument()) {
|
||||
document->match([&](const MTPDdocument &) {
|
||||
parsed.thumbnail = Ui::MakeDocumentThumbnail(
|
||||
_controller->session().data().processDocument(*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, std::nullopt, nullptr, false);
|
||||
showToast(tr::lng_attach_failed(tr::now));
|
||||
return;
|
||||
}
|
||||
setMedia(
|
||||
media,
|
||||
std::move(parsed.input),
|
||||
media->thumbnail
|
||||
? media->thumbnail
|
||||
: std::move(parsed.thumbnail),
|
||||
true);
|
||||
}).fail([=](const MTP::Error &) {
|
||||
if (media->token != token) {
|
||||
return;
|
||||
}
|
||||
setMedia(media, std::nullopt, 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, std::nullopt, nullptr, false);
|
||||
showToast(tr::lng_attach_failed(tr::now));
|
||||
}, lifetime());
|
||||
const auto choosePhoto = [=](std::shared_ptr<PollMediaState> media) {
|
||||
const auto callback = crl::guard(this, [=](
|
||||
FileDialog::OpenResult &&result) {
|
||||
const auto checkResult = [&](const Ui::PreparedList &list) {
|
||||
using namespace Ui;
|
||||
return (list.files.size() == 1)
|
||||
&& (list.files.front().type == PreparedFile::Type::Photo);
|
||||
};
|
||||
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 = std::move(list->files.front());
|
||||
const auto token = ++media->token;
|
||||
media->media = std::nullopt;
|
||||
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, std::nullopt, 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);
|
||||
}));
|
||||
});
|
||||
FileDialog::GetOpenPath(
|
||||
this,
|
||||
tr::lng_attach_photo(tr::now),
|
||||
FileDialog::ImagesFilter(),
|
||||
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, std::nullopt, nullptr, false);
|
||||
};
|
||||
const auto showMediaMenu = [=](
|
||||
not_null<Ui::RpWidget*> button,
|
||||
std::shared_ptr<PollMediaState> media) {
|
||||
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(tr::now),
|
||||
[=] { choosePhoto(media); },
|
||||
&st::menuIconPhoto);
|
||||
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();
|
||||
field->sizeValue(
|
||||
) | rpl::on_next([=](QSize size) {
|
||||
button->moveToRight(
|
||||
st::createPollOptionRemovePosition.x() + st::pollAttachShift.x(),
|
||||
((size.height() - button->height()) / 2)
|
||||
+ st::pollAttachShift.y(),
|
||||
size.width());
|
||||
}, button->lifetime());
|
||||
button->clicks(
|
||||
) | rpl::on_next([=](Qt::MouseButton buttonType) {
|
||||
if (buttonType != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
showMediaMenu(not_null<Ui::RpWidget*>(button), media);
|
||||
}, button->lifetime());
|
||||
};
|
||||
|
||||
const auto question = setupQuestion(container);
|
||||
const auto description = setupDescription(container);
|
||||
addMediaButton(description, state->descriptionMedia);
|
||||
Ui::AddDivider(container);
|
||||
Ui::AddSkip(container);
|
||||
container->add(
|
||||
@@ -1065,7 +1690,8 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
container,
|
||||
_controller,
|
||||
_emojiPanel ? _emojiPanel.get() : nullptr,
|
||||
(_chosen & PollData::Flag::Quiz));
|
||||
(_chosen & PollData::Flag::Quiz),
|
||||
showMediaMenu);
|
||||
const auto options = state->options.get();
|
||||
auto limit = options->usedCount() | rpl::after_next([=](int count) {
|
||||
setCloseByEscape(!count);
|
||||
@@ -1115,21 +1741,17 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
rpl::single(!!(_chosen & PollData::Flag::PublicVotes)),
|
||||
st::detailedSettingsButtonStyle)
|
||||
: nullptr;
|
||||
const auto hasMultiple = !(_chosen & PollData::Flag::Quiz)
|
||||
|| !(_disabled & PollData::Flag::Quiz);
|
||||
const auto multiple = hasMultiple
|
||||
? 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)
|
||||
: 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,
|
||||
@@ -1177,6 +1799,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
const auto solution = setupSolution(
|
||||
container,
|
||||
rpl::single(quiz->toggled()) | rpl::then(quiz->toggledChanges()));
|
||||
addMediaButton(solution, state->solutionMedia);
|
||||
|
||||
options->tabbed(
|
||||
) | rpl::on_next([=] {
|
||||
@@ -1199,25 +1822,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
}
|
||||
revoting->setToggleLocked(_disabled & PollData::Flag::RevotingDisabled);
|
||||
shuffle->setToggleLocked(_disabled & PollData::Flag::ShuffleAnswers);
|
||||
if (multiple) {
|
||||
multiple->setToggleLocked((_disabled & PollData::Flag::MultiChoice)
|
||||
|| (_chosen & PollData::Flag::Quiz));
|
||||
multiple->clickAreaEvents(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::MouseButtonPress)
|
||||
&& quiz->toggled();
|
||||
}) | rpl::on_next([show = uiShow()] {
|
||||
show->showToast(tr::lng_polls_create_one_answer(tr::now));
|
||||
}, multiple->lifetime());
|
||||
multiple->toggledChanges(
|
||||
) | rpl::on_next([=](bool checked) {
|
||||
if (checked
|
||||
&& (quiz->toggled()
|
||||
|| (_disabled & PollData::Flag::MultiChoice))) {
|
||||
state->multipleForceOff.fire(false);
|
||||
}
|
||||
}, multiple->lifetime());
|
||||
}
|
||||
multiple->setToggleLocked(_disabled & PollData::Flag::MultiChoice);
|
||||
|
||||
using namespace rpl::mappers;
|
||||
quiz->toggledChanges(
|
||||
@@ -1226,13 +1831,6 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
state->quizForceOff.fire(false);
|
||||
return;
|
||||
}
|
||||
if (multiple) {
|
||||
if (checked && multiple->toggled()) {
|
||||
state->multipleForceOff.fire(false);
|
||||
}
|
||||
multiple->setToggleLocked(checked
|
||||
|| (_disabled & PollData::Flag::MultiChoice));
|
||||
}
|
||||
options->enableChooseCorrect(checked);
|
||||
}, quiz->lifetime());
|
||||
|
||||
@@ -1273,8 +1871,12 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
solutionWithTags.text,
|
||||
TextUtilities::ConvertTextTagsToEntities(solutionWithTags.tags)
|
||||
};
|
||||
result.attachedMedia = state->descriptionMedia->media;
|
||||
if (quiz->toggled()) {
|
||||
result.solutionMedia = state->solutionMedia->media;
|
||||
}
|
||||
const auto publicVotes = (showWhoVoted && showWhoVoted->toggled());
|
||||
const auto multiChoice = (multiple && multiple->toggled());
|
||||
const auto multiChoice = multiple->toggled();
|
||||
result.setFlags(Flag(0)
|
||||
| (publicVotes ? Flag::PublicVotes : Flag(0))
|
||||
| (multiChoice ? Flag::MultiChoice : Flag(0))
|
||||
@@ -1318,6 +1920,13 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
} 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;
|
||||
}
|
||||
};
|
||||
const auto showError = [show = uiShow()](
|
||||
tr::phrase<> text) {
|
||||
@@ -1335,6 +1944,8 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
showError(tr::lng_polls_choose_correct);
|
||||
} else if (state->error & Error::Solution) {
|
||||
solution->showError();
|
||||
} else if (state->error & Error::Media) {
|
||||
showError(tr::lng_polls_media_uploading);
|
||||
} else if (!state->error) {
|
||||
auto result = collectResult();
|
||||
result.options = sendOptions;
|
||||
|
||||
@@ -30,6 +30,8 @@ namespace SendMenu {
|
||||
struct Details;
|
||||
} // namespace SendMenu
|
||||
|
||||
class PeerData;
|
||||
|
||||
class CreatePollBox : public Ui::BoxContent {
|
||||
public:
|
||||
struct Result {
|
||||
@@ -41,6 +43,7 @@ public:
|
||||
CreatePollBox(
|
||||
QWidget*,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<PeerData*> peer,
|
||||
PollData::Flags chosen,
|
||||
PollData::Flags disabled,
|
||||
rpl::producer<int> starsRequired,
|
||||
@@ -62,6 +65,7 @@ private:
|
||||
Correct = 0x04,
|
||||
Other = 0x08,
|
||||
Solution = 0x10,
|
||||
Media = 0x20,
|
||||
};
|
||||
friend constexpr inline bool is_flag_type(Error) { return true; }
|
||||
using Errors = base::flags<Error>;
|
||||
@@ -76,6 +80,7 @@ private:
|
||||
rpl::producer<bool> shown);
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
const not_null<PeerData*> _peer;
|
||||
const PollData::Flags _chosen = PollData::Flags();
|
||||
const PollData::Flags _disabled = PollData::Flags();
|
||||
const Api::SendType _sendType = Api::SendType();
|
||||
|
||||
@@ -26,6 +26,8 @@ pollBoxFilledChatlistMentionIcon: icon{{ "poll/filled/filled_chatlist_mention",
|
||||
pollBoxFilledPollMultipleIcon: icon{{ "poll/filled/filled_poll_multiple", activeButtonFg }};
|
||||
|
||||
pollAttachTextSkip: 28px;
|
||||
pollAttachProgressMargin: 4px;
|
||||
pollAttachShift: point(-11px, -2px);
|
||||
|
||||
pollDescriptionFieldPadding: margins(22px, 5px, 11px, 5px);
|
||||
pollDescriptionField: InputField(createPollField) {
|
||||
@@ -33,3 +35,19 @@ pollDescriptionField: InputField(createPollField) {
|
||||
border: 0px;
|
||||
borderActive: 0px;
|
||||
}
|
||||
|
||||
pollAttach: IconButton(defaultIconButton) {
|
||||
width: pollAttachTextSkip;
|
||||
height: pollAttachTextSkip;
|
||||
|
||||
icon: icon {{ "poll/general/outline_poll_attach-24x24", historyComposeIconFg }};
|
||||
iconOver: icon {{ "poll/general/outline_poll_attach-24x24", historyComposeIconFgOver }};
|
||||
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: pollAttachTextSkip;
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
||||
pollMediaField: InputField(createPollSolutionField) {
|
||||
textMargins: margins(0px, 4px, pollAttachTextSkip, 4px);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,10 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
||||
| (poll.is_quiz() ? Flag::Quiz : Flag(0))
|
||||
| (poll.is_shuffle_answers() ? Flag::ShuffleAnswers : Flag(0))
|
||||
| (poll.is_revoting_disabled() ? Flag::RevotingDisabled : Flag(0))
|
||||
| (poll.is_open_answers() ? Flag::OpenAnswers : Flag(0));
|
||||
| (poll.is_open_answers() ? Flag::OpenAnswers : Flag(0))
|
||||
| (poll.is_hide_results_until_close()
|
||||
? Flag::HideResultsUntilClose
|
||||
: Flag(0));
|
||||
const auto newCloseDate = poll.vclose_date().value_or_empty();
|
||||
const auto newClosePeriod = poll.vclose_period().value_or_empty();
|
||||
auto newAnswers = ranges::views::all(
|
||||
@@ -91,6 +94,18 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
||||
result.text = Api::ParseTextWithEntities(
|
||||
&session(),
|
||||
answer.vtext());
|
||||
if (const auto media = answer.vmedia()) {
|
||||
result.media = PollMediaToInputMedia(*media);
|
||||
}
|
||||
return result;
|
||||
}, [&](const MTPDinputPollAnswer &answer) {
|
||||
auto result = PollAnswer();
|
||||
result.text = Api::ParseTextWithEntities(
|
||||
&session(),
|
||||
answer.vtext());
|
||||
if (const auto media = answer.vmedia()) {
|
||||
result.media = *media;
|
||||
}
|
||||
return result;
|
||||
}, [](const auto &) {
|
||||
return PollAnswer();
|
||||
@@ -174,6 +189,16 @@ bool PollData::applyResults(const MTPPollResults &results) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (const auto media = results.vsolution_media()) {
|
||||
const auto parsed = PollMediaToInputMedia(*media);
|
||||
const auto changedMedia = !parsed
|
||||
? solutionMedia.has_value()
|
||||
: (!solutionMedia || (solutionMedia->type() != parsed->type()));
|
||||
solutionMedia = parsed;
|
||||
if (changedMedia) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
return false;
|
||||
}
|
||||
@@ -211,12 +236,12 @@ bool PollData::applyResultToAnswers(
|
||||
if (!answer) {
|
||||
return false;
|
||||
}
|
||||
const auto newVotes = voters.vvoters()
|
||||
? voters.vvoters()->v
|
||||
: 0;
|
||||
auto changed = (answer->votes != newVotes);
|
||||
if (changed) {
|
||||
answer->votes = newVotes;
|
||||
auto changed = false;
|
||||
if (const auto count = voters.vvoters()) {
|
||||
if (answer->votes != count->v) {
|
||||
answer->votes = count->v;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!isMinResults) {
|
||||
if (answer->chosen != voters.is_chosen()) {
|
||||
@@ -275,17 +300,68 @@ bool PollData::openAnswers() const {
|
||||
return (_flags & Flag::OpenAnswers);
|
||||
}
|
||||
|
||||
bool PollData::hideResultsUntilClose() const {
|
||||
return (_flags & Flag::HideResultsUntilClose);
|
||||
}
|
||||
|
||||
std::optional<MTPInputMedia> PollMediaToInputMedia(
|
||||
const MTPMessageMedia &media) {
|
||||
return media.match([&](const MTPDmessageMediaPhoto &media) {
|
||||
const auto photo = media.vphoto();
|
||||
if (!photo || photo->type() != mtpc_photo) {
|
||||
return std::optional<MTPInputMedia>();
|
||||
}
|
||||
const auto &fields = photo->c_photo();
|
||||
using Flag = MTPDinputMediaPhoto::Flag;
|
||||
const auto flags = media.vttl_seconds()
|
||||
? Flag::f_ttl_seconds
|
||||
: Flag(0);
|
||||
return std::optional<MTPInputMedia>(MTP_inputMediaPhoto(
|
||||
MTP_flags(flags),
|
||||
MTP_inputPhoto(
|
||||
fields.vid(),
|
||||
fields.vaccess_hash(),
|
||||
fields.vfile_reference()),
|
||||
MTP_int(media.vttl_seconds().value_or_empty()),
|
||||
MTPInputDocument()));
|
||||
}, [&](const MTPDmessageMediaDocument &media) {
|
||||
const auto document = media.vdocument();
|
||||
if (!document || document->type() != mtpc_document) {
|
||||
return std::optional<MTPInputMedia>();
|
||||
}
|
||||
const auto &fields = document->c_document();
|
||||
using Flag = MTPDinputMediaDocument::Flag;
|
||||
const auto flags = Flag()
|
||||
| (media.vttl_seconds() ? Flag::f_ttl_seconds : Flag())
|
||||
| (media.vvideo_timestamp()
|
||||
? Flag::f_video_timestamp
|
||||
: Flag());
|
||||
return std::optional<MTPInputMedia>(MTP_inputMediaDocument(
|
||||
MTP_flags(flags),
|
||||
MTP_inputDocument(
|
||||
fields.vid(),
|
||||
fields.vaccess_hash(),
|
||||
fields.vfile_reference()),
|
||||
MTPInputPhoto(),
|
||||
MTP_int(media.vvideo_timestamp().value_or_empty()),
|
||||
MTP_int(media.vttl_seconds().value_or_empty()),
|
||||
MTPstring()));
|
||||
}, [](const auto &) {
|
||||
return std::optional<MTPInputMedia>();
|
||||
});
|
||||
}
|
||||
|
||||
MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
|
||||
const auto convert = [&](const PollAnswer &answer) {
|
||||
return MTP_pollAnswer(
|
||||
MTP_flags(0),
|
||||
const auto flags = answer.media
|
||||
? MTPDinputPollAnswer::Flag::f_media
|
||||
: MTPDinputPollAnswer::Flag(0);
|
||||
return MTP_inputPollAnswer(
|
||||
MTP_flags(flags),
|
||||
MTP_textWithEntities(
|
||||
MTP_string(answer.text.text),
|
||||
Api::EntitiesToMTP(&poll->session(), answer.text.entities)),
|
||||
MTP_bytes(answer.option),
|
||||
MTPMessageMedia(), // media
|
||||
MTPPeer(), // added_by
|
||||
MTPint()); // date
|
||||
answer.media ? *answer.media : MTPInputMedia());
|
||||
};
|
||||
auto answers = QVector<MTPPollAnswer>();
|
||||
answers.reserve(poll->answers.size());
|
||||
@@ -301,6 +377,9 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
|
||||
| (poll->shuffleAnswers() ? Flag::f_shuffle_answers : Flag(0))
|
||||
| (poll->revotingDisabled() ? Flag::f_revoting_disabled : Flag(0))
|
||||
| (poll->openAnswers() ? Flag::f_open_answers : Flag(0))
|
||||
| (poll->hideResultsUntilClose()
|
||||
? Flag::f_hide_results_until_close
|
||||
: Flag(0))
|
||||
| (poll->closePeriod > 0 ? Flag::f_close_period : Flag(0))
|
||||
| (poll->closeDate > 0 ? Flag::f_close_date : Flag(0));
|
||||
return MTP_poll(
|
||||
@@ -343,12 +422,18 @@ MTPInputMedia PollDataToInputMedia(
|
||||
if (!sentEntities.v.isEmpty()) {
|
||||
inputFlags |= MTPDinputMediaPoll::Flag::f_solution_entities;
|
||||
}
|
||||
if (poll->attachedMedia) {
|
||||
inputFlags |= MTPDinputMediaPoll::Flag::f_attached_media;
|
||||
}
|
||||
if (poll->solutionMedia) {
|
||||
inputFlags |= MTPDinputMediaPoll::Flag::f_solution_media;
|
||||
}
|
||||
return MTP_inputMediaPoll(
|
||||
MTP_flags(inputFlags),
|
||||
PollDataToMTP(poll, close),
|
||||
MTP_vector<MTPint>(correct),
|
||||
MTPInputMedia(), // attached_media
|
||||
poll->attachedMedia ? *poll->attachedMedia : MTPInputMedia(),
|
||||
MTP_string(solution.text),
|
||||
sentEntities,
|
||||
MTPInputMedia()); // solution_media
|
||||
poll->solutionMedia ? *poll->solutionMedia : MTPInputMedia());
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class Session;
|
||||
struct PollAnswer {
|
||||
TextWithEntities text;
|
||||
QByteArray option;
|
||||
std::optional<MTPInputMedia> media;
|
||||
int votes = 0;
|
||||
bool chosen = false;
|
||||
bool correct = false;
|
||||
@@ -39,13 +40,14 @@ struct PollData {
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
enum class Flag {
|
||||
Closed = 0x01,
|
||||
PublicVotes = 0x02,
|
||||
MultiChoice = 0x04,
|
||||
Quiz = 0x08,
|
||||
ShuffleAnswers = 0x10,
|
||||
RevotingDisabled = 0x20,
|
||||
OpenAnswers = 0x40,
|
||||
Closed = 0x01,
|
||||
PublicVotes = 0x02,
|
||||
MultiChoice = 0x04,
|
||||
Quiz = 0x08,
|
||||
ShuffleAnswers = 0x10,
|
||||
RevotingDisabled = 0x20,
|
||||
OpenAnswers = 0x40,
|
||||
HideResultsUntilClose = 0x80,
|
||||
};
|
||||
friend inline constexpr bool is_flag_type(Flag) { return true; };
|
||||
using Flags = base::flags<Flag>;
|
||||
@@ -69,6 +71,7 @@ struct PollData {
|
||||
[[nodiscard]] bool shuffleAnswers() const;
|
||||
[[nodiscard]] bool revotingDisabled() const;
|
||||
[[nodiscard]] bool openAnswers() const;
|
||||
[[nodiscard]] bool hideResultsUntilClose() const;
|
||||
|
||||
PollId id = 0;
|
||||
TextWithEntities question;
|
||||
@@ -76,6 +79,8 @@ struct PollData {
|
||||
std::vector<not_null<PeerData*>> recentVoters;
|
||||
std::vector<QByteArray> sendingVotes;
|
||||
TextWithEntities solution;
|
||||
std::optional<MTPInputMedia> attachedMedia;
|
||||
std::optional<MTPInputMedia> solutionMedia;
|
||||
TimeId closePeriod = 0;
|
||||
TimeId closeDate = 0;
|
||||
int totalVoters = 0;
|
||||
@@ -100,3 +105,5 @@ private:
|
||||
[[nodiscard]] MTPInputMedia PollDataToInputMedia(
|
||||
not_null<const PollData*> poll,
|
||||
bool close = false);
|
||||
[[nodiscard]] std::optional<MTPInputMedia> PollMediaToInputMedia(
|
||||
const MTPMessageMedia &media);
|
||||
|
||||
@@ -4358,6 +4358,18 @@ not_null<PollData*> Session::processPoll(const MTPPoll &data) {
|
||||
|
||||
not_null<PollData*> Session::processPoll(const MTPDmessageMediaPoll &data) {
|
||||
const auto result = processPoll(data.vpoll());
|
||||
const auto media = data.vattached_media()
|
||||
? PollMediaToInputMedia(*data.vattached_media())
|
||||
: std::optional<MTPInputMedia>();
|
||||
const auto changedMedia = !media
|
||||
? result->attachedMedia.has_value()
|
||||
: (!result->attachedMedia
|
||||
|| (result->attachedMedia->type() != media->type()));
|
||||
result->attachedMedia = media;
|
||||
if (changedMedia) {
|
||||
++result->version;
|
||||
notifyPollUpdateDelayed(result);
|
||||
}
|
||||
const auto changed = result->applyResults(data.vresults());
|
||||
if (changed) {
|
||||
notifyPollUpdateDelayed(result);
|
||||
|
||||
@@ -302,6 +302,9 @@ QSize Poll::countOptimalSize() {
|
||||
}
|
||||
|
||||
bool Poll::showVotes() const {
|
||||
if (_flags & PollData::Flag::HideResultsUntilClose) {
|
||||
return (_flags & PollData::Flag::Closed) || _parent->data()->out();
|
||||
}
|
||||
return _voted || (_flags & PollData::Flag::Closed);
|
||||
}
|
||||
|
||||
|
||||
@@ -2214,6 +2214,7 @@ void PeerMenuCreatePoll(
|
||||
});
|
||||
auto box = Box<CreatePollBox>(
|
||||
controller,
|
||||
peer,
|
||||
chosen,
|
||||
disabled,
|
||||
std::move(starsRequired),
|
||||
|
||||
Reference in New Issue
Block a user