[poll-create] Added files support for poll media.

This commit is contained in:
23rd
2026-03-19 18:09:26 +03:00
parent 9e482556ea
commit 8145b0b96e
9 changed files with 816 additions and 73 deletions
+1
View File
@@ -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";
+662 -51
View File
@@ -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();
+18
View File
@@ -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);
}
+100 -15
View File
@@ -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());
}
+14 -7
View File
@@ -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),